├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── config.yml
│ └── bug_report.yml
└── workflows
│ ├── pr.yml
│ └── ci.yml
├── .npmrc
├── examples
├── astro-react
│ ├── src
│ │ ├── env.d.ts
│ │ ├── app
│ │ │ ├── secret.secret$.ts
│ │ │ ├── entry-client.tsx
│ │ │ ├── manifest.tsx
│ │ │ ├── entry-server.tsx
│ │ │ └── root.tsx
│ │ └── pages
│ │ │ ├── [...app].ts
│ │ │ └── hello.astro
│ ├── tsconfig.json
│ ├── .vscode
│ │ ├── extensions.json
│ │ └── launch.json
│ ├── .gitignore
│ ├── astro.config.mjs
│ ├── package.json
│ ├── public
│ │ └── favicon.svg
│ └── README.md
├── astro-solid
│ ├── src
│ │ ├── env.d.ts
│ │ ├── app
│ │ │ ├── server.secret$.ts
│ │ │ ├── manifest.tsx
│ │ │ ├── entry-client.tsx
│ │ │ ├── entry-server.tsx
│ │ │ └── root.tsx
│ │ └── pages
│ │ │ ├── [...app].ts
│ │ │ └── hello.astro
│ ├── .vscode
│ │ ├── extensions.json
│ │ └── launch.json
│ ├── tsconfig.json
│ ├── .gitignore
│ ├── astro.config.mjs
│ ├── package.json
│ ├── public
│ │ └── favicon.svg
│ └── README.md
├── astro-react-router
│ ├── src
│ │ ├── env.d.ts
│ │ ├── app
│ │ │ ├── secret.secret$.ts
│ │ │ ├── manifest.tsx
│ │ │ ├── entry-client.tsx
│ │ │ ├── entry-server.tsx
│ │ │ └── root.tsx
│ │ └── pages
│ │ │ └── [...app].ts
│ ├── tsconfig.json
│ ├── .vscode
│ │ ├── extensions.json
│ │ └── launch.json
│ ├── .gitignore
│ ├── astro.config.mjs
│ ├── package.json
│ ├── public
│ │ └── favicon.svg
│ └── README.md
├── astro-solid-router
│ ├── src
│ │ ├── env.d.ts
│ │ ├── pages
│ │ │ ├── [...app].ts
│ │ │ └── hello.astro
│ │ └── app
│ │ │ ├── manifest.tsx
│ │ │ ├── data
│ │ │ ├── index.ts
│ │ │ ├── FormError.tsx
│ │ │ ├── LICENSE.remix.md
│ │ │ ├── useLoader.ts
│ │ │ ├── useAction.tsx
│ │ │ └── Form.tsx
│ │ │ ├── entry-client.tsx
│ │ │ ├── entry-server.tsx
│ │ │ └── root.tsx
│ ├── .vscode
│ │ ├── extensions.json
│ │ └── launch.json
│ ├── tsconfig.json
│ ├── .gitignore
│ ├── astro.config.mjs
│ ├── package.json
│ ├── public
│ │ └── favicon.svg
│ └── README.md
├── astro-react-todomvc
│ ├── src
│ │ ├── env.d.ts
│ │ ├── app
│ │ │ ├── db.secret$.ts
│ │ │ ├── manifest.tsx
│ │ │ ├── entry-client.tsx
│ │ │ ├── entry-server.tsx
│ │ │ └── root.tsx
│ │ ├── pages
│ │ │ └── [...app].ts
│ │ └── app.css
│ ├── tsconfig.json
│ ├── prisma
│ │ ├── dev.db
│ │ └── schema.prisma
│ ├── .vscode
│ │ ├── extensions.json
│ │ └── launch.json
│ ├── .gitignore
│ ├── astro.config.mjs
│ ├── public
│ │ └── favicon.svg
│ ├── package.json
│ └── README.md
└── astro-solid-hackernews
│ ├── src
│ ├── env.d.ts
│ ├── pages
│ │ ├── [...app].ts
│ │ └── hello.astro
│ └── app
│ │ ├── manifest.tsx
│ │ ├── types.ts
│ │ ├── entry-client.tsx
│ │ ├── components
│ │ ├── toggle.tsx
│ │ ├── nav.tsx
│ │ ├── comment.tsx
│ │ └── story.tsx
│ │ ├── entry-server.tsx
│ │ ├── root.css
│ │ └── root.tsx
│ ├── .vscode
│ ├── extensions.json
│ └── launch.json
│ ├── tsconfig.json
│ ├── .gitignore
│ ├── astro.config.mjs
│ ├── package.json
│ ├── public
│ └── favicon.svg
│ └── README.md
├── pnpm-workspace.yaml
├── scripts
├── build.ts
├── builder.ts
├── tsconfig.json
├── config.ts
└── types.ts
├── media
├── header.png
├── logo.sketch
├── repo-dark.png
└── logo.svg
├── .prettierrc
├── CONTRIBUTING.md
├── docs
├── overview.md
├── installation.md
└── config.json
├── packages
└── bling
│ ├── tsconfig.json
│ ├── src
│ ├── astro.ts
│ ├── types.ts
│ ├── vite.ts
│ ├── client.ts
│ ├── utils
│ │ └── utils.ts
│ └── server.ts
│ └── package.json
├── .gitignore
├── LICENSE
├── package.json
└── README.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: tannerlinsley
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | //registry.npmjs.org/:_authToken=${NPM_TOKEN}
--------------------------------------------------------------------------------
/examples/astro-react/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/astro-solid/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "packages/*"
3 | - "examples/*"
4 |
--------------------------------------------------------------------------------
/scripts/build.ts:
--------------------------------------------------------------------------------
1 | import { builder } from './builder'
2 |
3 | builder()
4 |
--------------------------------------------------------------------------------
/examples/astro-react-router/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/media/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanStack/bling/HEAD/media/header.png
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/media/logo.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanStack/bling/HEAD/media/logo.sketch
--------------------------------------------------------------------------------
/media/repo-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanStack/bling/HEAD/media/repo-dark.png
--------------------------------------------------------------------------------
/examples/astro-react-router/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strictest"
3 | }
--------------------------------------------------------------------------------
/examples/astro-react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strictest"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strictest"
3 | }
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "trailingComma": "all"
5 | }
6 |
--------------------------------------------------------------------------------
/examples/astro-react/src/app/secret.secret$.ts:
--------------------------------------------------------------------------------
1 | export const secret = 'This is a server-only secret!'
2 |
--------------------------------------------------------------------------------
/examples/astro-solid/src/app/server.secret$.ts:
--------------------------------------------------------------------------------
1 | export const secret = 'This is a server-only secret!'
2 |
--------------------------------------------------------------------------------
/examples/astro-react-router/src/app/secret.secret$.ts:
--------------------------------------------------------------------------------
1 | export const secret = 'This is a server-only secret!'
2 |
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/prisma/dev.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TanStack/bling/HEAD/examples/astro-react-todomvc/prisma/dev.db
--------------------------------------------------------------------------------
/examples/astro-react/src/pages/[...app].ts:
--------------------------------------------------------------------------------
1 | import { requestHandler } from '../app/entry-server'
2 |
3 | export const all = requestHandler
4 |
--------------------------------------------------------------------------------
/examples/astro-solid/src/pages/[...app].ts:
--------------------------------------------------------------------------------
1 | import { requestHandler } from '../app/entry-server'
2 |
3 | export const all = requestHandler
4 |
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/src/app/db.secret$.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client'
2 | export const prisma = new PrismaClient()
3 |
--------------------------------------------------------------------------------
/examples/astro-react/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/examples/astro-react/src/pages/hello.astro:
--------------------------------------------------------------------------------
1 | ---
2 | console.log('hello')
3 | ---
4 |
5 |
6 |
Hello!
7 |
8 |
--------------------------------------------------------------------------------
/examples/astro-solid/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/examples/astro-solid/src/pages/hello.astro:
--------------------------------------------------------------------------------
1 | ---
2 | console.log('hello')
3 | ---
4 |
5 |
6 | Hello!
7 |
8 |
--------------------------------------------------------------------------------
/examples/astro-react-router/src/pages/[...app].ts:
--------------------------------------------------------------------------------
1 | import { requestHandler } from '../app/entry-server'
2 |
3 | export const all = requestHandler
4 |
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/src/pages/[...app].ts:
--------------------------------------------------------------------------------
1 | import { requestHandler } from '../app/entry-server'
2 |
3 | export const all = requestHandler
4 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/src/pages/[...app].ts:
--------------------------------------------------------------------------------
1 | import { requestHandler } from '../app/entry-server'
2 |
3 | export const all = requestHandler
4 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/src/pages/[...app].ts:
--------------------------------------------------------------------------------
1 | import { requestHandler } from '../app/entry-server'
2 |
3 | export const all = requestHandler
4 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | - Clone the repo
4 | - Make changes
5 | - Run `npm run build` to build the dist files
6 | - Submit a pull request
7 |
--------------------------------------------------------------------------------
/examples/astro-react-router/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/src/pages/hello.astro:
--------------------------------------------------------------------------------
1 | ---
2 | console.log('hello')
3 | ---
4 |
5 |
6 | Hello!
7 |
8 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/src/pages/hello.astro:
--------------------------------------------------------------------------------
1 | ---
2 | console.log('hello')
3 | ---
4 |
5 |
6 | Hello!
7 |
8 |
--------------------------------------------------------------------------------
/examples/astro-react/src/app/entry-client.tsx:
--------------------------------------------------------------------------------
1 | import * as ReactDOM from 'react-dom/client'
2 | import { App } from './root'
3 |
4 | ReactDOM.hydrateRoot(document, )
5 |
--------------------------------------------------------------------------------
/examples/astro-solid/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strictest",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "jsxImportSource": "solid-js"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/base",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "jsxImportSource": "solid-js"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/docs/overview.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Overview
3 | ---
4 |
5 | TanStack Bling is a collection of framework agnostic utilities for client/server RPCs, env isolation, islands, module splitting, and more.
6 |
--------------------------------------------------------------------------------
/examples/astro-react/src/app/manifest.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 |
3 | export const manifestContext = createContext<{ 'entry-client': string }>({
4 | 'entry-client': '',
5 | })
6 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strictest",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "jsxImportSource": "solid-js"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/astro-solid/src/app/manifest.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from 'solid-js'
2 |
3 | export const manifestContext = createContext<{ 'entry-client': string }>({
4 | 'entry-client': '',
5 | })
6 |
--------------------------------------------------------------------------------
/examples/astro-react-router/src/app/manifest.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 |
3 | export const manifestContext = createContext<{ 'entry-client': string }>({
4 | 'entry-client': '',
5 | })
6 |
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/src/app/manifest.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 |
3 | export const manifestContext = createContext<{ 'entry-client': string }>({
4 | 'entry-client': '',
5 | })
6 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/src/app/manifest.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from 'solid-js'
2 |
3 | export const manifestContext = createContext<{ 'entry-client': string }>({
4 | 'entry-client': '',
5 | })
6 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Installation
3 | ---
4 |
5 | You can install TanStack Bling with any [NPM](https://npmjs.com) package manager.
6 |
7 | ```bash
8 | npm install @tanstack/bling
9 | ```
10 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/src/app/manifest.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from 'solid-js'
2 |
3 | export const manifestContext = createContext<{ 'entry-client': string }>({
4 | 'entry-client': '',
5 | })
6 |
--------------------------------------------------------------------------------
/examples/astro-react/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/examples/astro-solid/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/examples/astro-react-router/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/examples/astro-react/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | .output/
4 |
5 | # dependencies
6 | node_modules/
7 |
8 | # logs
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | pnpm-debug.log*
13 |
14 |
15 | # environment variables
16 | .env
17 | .env.production
18 |
19 | # macOS-specific files
20 | .DS_Store
21 |
--------------------------------------------------------------------------------
/examples/astro-solid/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | .output/
4 |
5 | # dependencies
6 | node_modules/
7 |
8 | # logs
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | pnpm-debug.log*
13 |
14 |
15 | # environment variables
16 | .env
17 | .env.production
18 |
19 | # macOS-specific files
20 | .DS_Store
21 |
--------------------------------------------------------------------------------
/examples/astro-solid/src/app/entry-client.tsx:
--------------------------------------------------------------------------------
1 | import { hydrate } from 'solid-js/web'
2 | import { manifestContext } from './manifest'
3 | import { App } from './root'
4 |
5 | hydrate(
6 | () => (
7 |
8 |
9 |
10 | ),
11 | document,
12 | )
13 |
--------------------------------------------------------------------------------
/examples/astro-react-router/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | .output/
4 |
5 | # dependencies
6 | node_modules/
7 |
8 | # logs
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | pnpm-debug.log*
13 |
14 |
15 | # environment variables
16 | .env
17 | .env.production
18 |
19 | # macOS-specific files
20 | .DS_Store
21 |
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | .output/
4 |
5 | # dependencies
6 | node_modules/
7 |
8 | # logs
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | pnpm-debug.log*
13 |
14 |
15 | # environment variables
16 | .env
17 | .env.production
18 |
19 | # macOS-specific files
20 | .DS_Store
21 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | .output/
4 |
5 | # dependencies
6 | node_modules/
7 |
8 | # logs
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | pnpm-debug.log*
13 |
14 |
15 | # environment variables
16 | .env
17 | .env.production
18 |
19 | # macOS-specific files
20 | .DS_Store
21 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | .output/
4 |
5 | # dependencies
6 | node_modules/
7 |
8 | # logs
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | pnpm-debug.log*
13 |
14 |
15 | # environment variables
16 | .env
17 | .env.production
18 |
19 | # macOS-specific files
20 | .DS_Store
21 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/src/app/data/index.ts:
--------------------------------------------------------------------------------
1 | export { useRouteData } from '@solidjs/router'
2 | export { useAction, useMultiAction as useMultiAction } from './useAction'
3 | export { useLoader, refetchLoaders } from './useLoader'
4 | export type { FormAction, FormMethod, FormProps, SubmitOptions } from './Form'
5 | export { FormError, ServerError } from './FormError'
6 |
--------------------------------------------------------------------------------
/packages/bling/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["DOM", "DOM.Iterable", "ES6"],
4 | "target": "ESNext",
5 | "module": "ES2020",
6 | "moduleResolution": "node",
7 | "strict": true,
8 | "outDir": "./dist",
9 | "skipLibCheck": true,
10 | "typeRoots": ["./node_modules/@types", "./src/@types"]
11 | },
12 | "include": ["src"]
13 | }
14 |
--------------------------------------------------------------------------------
/examples/astro-react-router/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'astro/config'
2 | import react from '@astrojs/react'
3 | import node from '@astrojs/node'
4 | import { astroBling } from '@tanstack/bling/astro'
5 |
6 | export default defineConfig({
7 | output: 'server',
8 | adapter: node({
9 | mode: 'standalone',
10 | }),
11 | integrations: [astroBling(), react()],
12 | })
13 |
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'astro/config'
2 | import react from '@astrojs/react'
3 | import node from '@astrojs/node'
4 | import { astroBling } from '@tanstack/bling/astro'
5 |
6 | export default defineConfig({
7 | output: 'server',
8 | adapter: node({
9 | mode: 'standalone',
10 | }),
11 | integrations: [astroBling(), react()],
12 | })
13 |
--------------------------------------------------------------------------------
/examples/astro-react/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'astro/config'
2 | import react from '@astrojs/react'
3 | import node from '@astrojs/node'
4 | import { astroBling } from '@tanstack/bling/astro'
5 |
6 | export default defineConfig({
7 | output: 'server',
8 | adapter: node({
9 | mode: 'standalone',
10 | }),
11 |
12 | integrations: [astroBling(), react()],
13 | })
14 |
--------------------------------------------------------------------------------
/examples/astro-solid/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'astro/config'
2 | import node from '@astrojs/node'
3 | import { astroBling } from '@tanstack/bling/astro'
4 | import solidJs from '@astrojs/solid-js'
5 |
6 | // https://astro.build/config
7 | export default defineConfig({
8 | output: 'server',
9 | adapter: node({
10 | mode: 'standalone',
11 | }),
12 | integrations: [astroBling(), solidJs()],
13 | })
14 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'astro/config'
2 | import node from '@astrojs/node'
3 | import { astroBling } from '@tanstack/bling/astro'
4 | import solidJs from '@astrojs/solid-js'
5 |
6 | // https://astro.build/config
7 | export default defineConfig({
8 | output: 'server',
9 | adapter: node({
10 | mode: 'standalone',
11 | }),
12 | integrations: [astroBling(), solidJs()],
13 | })
14 |
--------------------------------------------------------------------------------
/scripts/builder.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from 'child_process'
2 | import { packages } from './config'
3 | import { Package } from './types'
4 |
5 | export async function builder() {
6 | await Promise.all(
7 | packages.map(async (pkg) => {
8 | buildPackage(pkg)
9 | })
10 | )
11 | }
12 |
13 | function buildPackage(pkg: Package) {
14 | execSync(`cd packages/${pkg.packageDir} && yarn build`, { stdio: 'inherit' })
15 | }
16 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'astro/config'
2 | import node from '@astrojs/node'
3 | import { astroBling } from '@tanstack/bling/astro'
4 | import solidJs from '@astrojs/solid-js'
5 |
6 | // https://astro.build/config
7 | export default defineConfig({
8 | output: 'server',
9 | adapter: node({
10 | mode: 'standalone',
11 | }),
12 | integrations: [astroBling(), solidJs()],
13 | })
14 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/src/app/types.ts:
--------------------------------------------------------------------------------
1 | export interface IComment {
2 | user: string;
3 | time_ago: string;
4 | content: string;
5 | comments: IComment[];
6 | }
7 |
8 | export interface IStory {
9 | id: string;
10 | points: string;
11 | url: string;
12 | title: string;
13 | domain: string;
14 | type: string;
15 | time_ago: string;
16 | user: string;
17 | comments_count: number;
18 | comments: IComment[];
19 | }
20 |
--------------------------------------------------------------------------------
/examples/astro-react-router/src/app/entry-client.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client'
2 | import { createBrowserRouter, RouterProvider } from 'react-router-dom'
3 | import { routes } from './root'
4 | import { addSerializer } from '@tanstack/bling/client'
5 |
6 | addSerializer({
7 | apply: (req) => req instanceof Request,
8 | serialize: (value) => '$request',
9 | })
10 |
11 | let router = createBrowserRouter(routes)
12 |
13 | ReactDOM.hydrateRoot(document, )
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: 🤔 Feature Requests & Questions
4 | url: https://github.com/tanstack/router/discussions
5 | about: Please ask and answer questions here.
6 | - name: 💬 Community Chat
7 | url: https://discord.gg/mQd7egN
8 | about: A dedicated discord server hosted by Tanner Linsley
9 | - name: 💬 Tanstack Twitter
10 | url: https://twitter.com/tan_stack
11 | about: Stay up to date with new releases of our libraries
12 |
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | }
7 |
8 | datasource db {
9 | provider = "sqlite"
10 | url = "file:./dev.db"
11 | }
12 |
13 | model Todo {
14 | id String @id @default(cuid())
15 | title String
16 | complete Boolean
17 | createdAt DateTime @default(now())
18 | updatedAt DateTime @updatedAt
19 | }
20 |
--------------------------------------------------------------------------------
/scripts/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "commonjs",
5 | "esModuleInterop": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "strict": true,
8 | "noImplicitAny": true,
9 | "skipLibCheck": true,
10 | "checkJs": true,
11 | "types": ["node"]
12 | },
13 | "ts-node": {
14 | "transpileOnly": true,
15 | "files": true,
16 | "compilerOptions": {
17 | "sourceMap": true,
18 | "inlineSources": true
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/src/app/entry-client.tsx:
--------------------------------------------------------------------------------
1 | import { Router, useRoutes } from '@solidjs/router'
2 | import { hydrate } from 'solid-js/web'
3 | import { manifestContext } from './manifest'
4 | import { manifest } from 'astro:ssr-manifest'
5 | import { routes } from './root'
6 |
7 | hydrate(() => {
8 | const Routes = useRoutes(routes)
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | )
16 | }, document)
17 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/src/app/entry-client.tsx:
--------------------------------------------------------------------------------
1 | import { Router, useRoutes } from '@solidjs/router'
2 | import { hydrate } from 'solid-js/web'
3 | import { manifestContext } from './manifest'
4 | import { manifest } from 'astro:ssr-manifest'
5 | import { routes } from './root'
6 |
7 | hydrate(() => {
8 | const Routes = useRoutes(routes)
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | )
16 | }, document)
17 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/src/app/components/toggle.tsx:
--------------------------------------------------------------------------------
1 | import { createSignal } from "solid-js";
2 |
3 | export default function Toggle(props: { children: any }) {
4 | const [open, setOpen] = createSignal(true);
5 |
6 | return (
7 | <>
8 |
11 |
14 | >
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/docs/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "docSearch": {
3 | "appId": "",
4 | "apiKey": "",
5 | "indexName": ""
6 | },
7 | "menu": [
8 | {
9 | "label": "Getting Started",
10 | "children": [
11 | {
12 | "label": "Overview",
13 | "to": "overview"
14 | },
15 | {
16 | "label": "Installation",
17 | "to": "installation"
18 | }
19 | ]
20 | },
21 | {
22 | "label": "Guide",
23 | "children": []
24 | },
25 | {
26 | "label": "API",
27 | "children": []
28 | }
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/src/app/entry-client.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client'
2 | import { createBrowserRouter, RouterProvider } from 'react-router-dom'
3 | import { routes } from './root'
4 | import { addSerializer } from '@tanstack/bling/client'
5 | import { QueryClientProvider } from '@tanstack/react-query'
6 |
7 | addSerializer({
8 | apply: (req) => req instanceof Request,
9 | serialize: (value) => '$request',
10 | })
11 |
12 | let router = createBrowserRouter(routes)
13 |
14 | function Client() {
15 | return
16 | }
17 |
18 | ReactDOM.hydrateRoot(document, )
19 |
--------------------------------------------------------------------------------
/examples/astro-solid/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example-astro-solid",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "private": true,
6 | "scripts": {
7 | "dev": "astro dev",
8 | "start": "npm run dev",
9 | "build": "astro build",
10 | "preview": "astro preview",
11 | "astro": "astro"
12 | },
13 | "dependencies": {
14 | "@astrojs/netlify": "^1.2.1",
15 | "@astrojs/node": "^5.0.3",
16 | "@astrojs/solid-js": "^1.2.3",
17 | "@babel/generator": "^7.21.1",
18 | "@tanstack/bling": "^0.1.3",
19 | "astro": "0.0.0-ssr-manifest-20230306183729",
20 | "concurrently": "^7.6.0",
21 | "solid-js": "^1.4.3"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example-astro-solid-router",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "private": true,
6 | "scripts": {
7 | "dev": "astro dev",
8 | "start": "npm run dev",
9 | "build": "astro build",
10 | "preview": "astro preview",
11 | "astro": "astro"
12 | },
13 | "dependencies": {
14 | "@astrojs/netlify": "^1.2.1",
15 | "@astrojs/node": "^5.0.3",
16 | "@astrojs/solid-js": "^1.2.3",
17 | "@babel/generator": "^7.21.1",
18 | "@solidjs/router": "^0.7.0",
19 | "@tanstack/bling": "^0.1.3",
20 | "astro": "0.0.0-ssr-manifest-20230306183729",
21 | "concurrently": "^7.6.0",
22 | "solid-js": "^1.4.3"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example-astro-solid-hackernews",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "private": true,
6 | "scripts": {
7 | "dev": "astro dev",
8 | "start": "npm run dev",
9 | "build": "astro build",
10 | "preview": "astro preview",
11 | "astro": "astro"
12 | },
13 | "dependencies": {
14 | "@astrojs/netlify": "^1.2.1",
15 | "@astrojs/node": "^5.0.3",
16 | "@astrojs/solid-js": "^1.2.3",
17 | "@babel/generator": "^7.21.1",
18 | "@solidjs/router": "^0.7.0",
19 | "@tanstack/bling": "^0.1.3",
20 | "astro": "0.0.0-ssr-manifest-20230306183729",
21 | "concurrently": "^7.6.0",
22 | "solid-js": "^1.4.3"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vite
2 | # See https://help.github.com/ignore-files/ for more about ignoring files.
3 |
4 | # dependencies
5 | node_modules
6 |
7 | # builds
8 | types
9 | build
10 | */build
11 | dist
12 | lib
13 | es
14 | artifacts
15 | .rpt2_cache
16 | coverage
17 | *.tgz
18 |
19 | # misc
20 | .DS_Store
21 | .env
22 | .env.local
23 | .env.development.local
24 | .env.test.local
25 | .env.production.local
26 | .next
27 |
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .history
32 | size-plugin.json
33 | stats-hydration.json
34 | stats-react.json
35 | stats.html
36 | .vscode/settings.json
37 |
38 | *.log
39 | .DS_Store
40 | node_modules
41 | .cache
42 | dist
43 | ts-perf
44 |
45 | /examples/*/*/yarn.lock
46 | /examples/*/*/package-lock.json
47 |
48 | .netlify
49 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/src/app/components/nav.tsx:
--------------------------------------------------------------------------------
1 | import { A } from "@solidjs/router";
2 |
3 | function Nav() {
4 | return (
5 |
27 | );
28 | }
29 |
30 | export default Nav;
31 |
--------------------------------------------------------------------------------
/examples/astro-react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example-astro-react",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "private": true,
6 | "scripts": {
7 | "dev": "astro dev",
8 | "start": "npm run dev",
9 | "build": "astro build",
10 | "preview": "astro preview",
11 | "astro": "astro"
12 | },
13 | "dependencies": {
14 | "@astrojs/netlify": "^1.2.1",
15 | "@astrojs/node": "^5.0.3",
16 | "@astrojs/react": "^1.2.2",
17 | "@babel/generator": "^7.21.1",
18 | "@tanstack/bling": "^0.1.3",
19 | "@vitejs/plugin-react": "^3.1.0",
20 | "astro": "0.0.0-ssr-manifest-20230306183729",
21 | "concurrently": "^7.6.0",
22 | "react": "^18.2.0",
23 | "react-dom": "^18.2.0"
24 | },
25 | "devDependencies": {
26 | "@types/react": "^18.0.28",
27 | "@types/react-dom": "^18.0.11"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/examples/astro-solid/src/app/entry-server.tsx:
--------------------------------------------------------------------------------
1 | import { hasHandler, handleFetch$ } from '@tanstack/bling/server'
2 | import type { APIContext } from 'astro'
3 | import { renderToStringAsync } from 'solid-js/web'
4 | import { App } from './root'
5 | import { manifest } from 'astro:ssr-manifest'
6 | import { manifestContext } from './manifest'
7 |
8 | export const requestHandler = async ({ request }: APIContext) => {
9 | if (hasHandler(new URL(request.url).pathname)) {
10 | return await handleFetch$({
11 | request,
12 | })
13 | }
14 |
15 | return new Response(
16 | await renderToStringAsync(() => (
17 |
18 |
19 |
20 | )),
21 | {
22 | headers: {
23 | 'content-type': 'text/html',
24 | },
25 | },
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/examples/astro-react/src/app/entry-server.tsx:
--------------------------------------------------------------------------------
1 | import { hasHandler, handleFetch$ } from '@tanstack/bling/server'
2 | import type { APIContext } from 'astro'
3 | import * as ReactDOM from 'react-dom/server.browser'
4 | import { App } from './root'
5 | import { manifest } from 'astro:ssr-manifest'
6 | import { manifestContext } from './manifest'
7 |
8 | export const requestHandler = async ({ request }: APIContext) => {
9 | if (hasHandler(new URL(request.url).pathname)) {
10 | return await handleFetch$({
11 | request,
12 | })
13 | }
14 |
15 | return new Response(
16 | await ReactDOM.renderToReadableStream(
17 |
18 |
19 | ,
20 | ),
21 | {
22 | headers: {
23 | 'content-type': 'text/html',
24 | },
25 | },
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/src/app/components/comment.tsx:
--------------------------------------------------------------------------------
1 | import { Component, For, Show } from 'solid-js'
2 | import { A } from '@solidjs/router'
3 | import type { IComment } from '../types'
4 | import Toggle from './toggle'
5 |
6 | const Comment: Component<{ comment: IComment }> = (props) => {
7 | return (
8 |
22 | )
23 | }
24 |
25 | export default Comment
26 |
--------------------------------------------------------------------------------
/scripts/config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import { BranchConfig, Package } from './types'
3 |
4 | // TODO: List your npm packages here.
5 | export const packages: Package[] = [
6 | {
7 | name: '@tanstack/bling',
8 | packageDir: 'bling',
9 | srcDir: 'src',
10 | },
11 | ]
12 |
13 | export const latestBranch = 'main'
14 |
15 | export const branchConfigs: Record = {
16 | main: {
17 | prerelease: false,
18 | ghRelease: true,
19 | },
20 | next: {
21 | prerelease: true,
22 | ghRelease: true,
23 | },
24 | beta: {
25 | prerelease: true,
26 | ghRelease: true,
27 | },
28 | alpha: {
29 | prerelease: true,
30 | ghRelease: true,
31 | },
32 | }
33 |
34 | export const rootDir = path.resolve(__dirname, '..')
35 | export const examplesDirs = [
36 | // 'examples/react',
37 | // 'examples/solid',
38 | // 'examples/svelte',
39 | // 'examples/vue',
40 | ]
41 |
--------------------------------------------------------------------------------
/examples/astro-react-router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example-astro-react-router",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "private": true,
6 | "scripts": {
7 | "dev": "astro dev",
8 | "start": "npm run dev",
9 | "build": "astro build",
10 | "preview": "astro preview",
11 | "astro": "astro"
12 | },
13 | "dependencies": {
14 | "@astrojs/netlify": "^1.2.1",
15 | "@astrojs/node": "^5.0.3",
16 | "@astrojs/react": "^1.2.2",
17 | "@babel/generator": "^7.21.1",
18 | "@remix-run/router": "^1.3.3",
19 | "@tanstack/bling": "^0.1.3",
20 | "@vitejs/plugin-react": "^3.1.0",
21 | "astro": "0.0.0-ssr-manifest-20230306183729",
22 | "concurrently": "^7.6.0",
23 | "react": "^18.2.0",
24 | "react-dom": "^18.2.0",
25 | "react-router-dom": "^6.8.2"
26 | },
27 | "devDependencies": {
28 | "@types/react": "^18.0.28",
29 | "@types/react-dom": "^18.0.11"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/examples/astro-react/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/examples/astro-solid/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/scripts/types.ts:
--------------------------------------------------------------------------------
1 | export type Commit = {
2 | commit: CommitOrTree
3 | tree: CommitOrTree
4 | author: AuthorOrCommitter
5 | committer: AuthorOrCommitter
6 | subject: string
7 | body: string
8 | parsed: Parsed
9 | }
10 |
11 | export type CommitOrTree = {
12 | long: string
13 | short: string
14 | }
15 |
16 | export type AuthorOrCommitter = {
17 | name: string
18 | email: string
19 | date: string
20 | }
21 |
22 | export type Parsed = {
23 | type: string
24 | scope?: string | null
25 | subject: string
26 | merge?: null
27 | header: string
28 | body?: null
29 | footer?: null
30 | notes?: null[] | null
31 | references?: null[] | null
32 | mentions?: null[] | null
33 | revert?: null
34 | raw: string
35 | }
36 |
37 | export type Package = {
38 | name: string
39 | packageDir: string
40 | srcDir: string
41 | }
42 |
43 | export type BranchConfig = {
44 | prerelease: boolean
45 | ghRelease: boolean
46 | }
47 |
--------------------------------------------------------------------------------
/examples/astro-react-router/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/src/app/data/FormError.tsx:
--------------------------------------------------------------------------------
1 | export class ServerError extends Error {
2 | status: number;
3 | constructor(message: string, { status, stack }: { status?: number; stack?: string } = {}) {
4 | super(message);
5 | this.name = "ServerError";
6 | this.status = status || 400
7 | if (stack) {
8 | this.stack = stack;
9 | }
10 | }
11 | }
12 |
13 | export class FormError extends ServerError {
14 | formError?: string;
15 | fields?: {};
16 | fieldErrors?: { [key: string]: string };
17 | constructor(
18 | message: string,
19 | {
20 | fieldErrors = {},
21 | form,
22 | fields,
23 | stack
24 | }: { fieldErrors?: {}; form?: FormData; fields?: {}; stack?: string } = {}
25 | ) {
26 | super(message, { stack });
27 | this.formError = message;
28 | this.name = "FormError";
29 | this.fields =
30 | fields || Object.fromEntries(typeof form !== "undefined" ? form.entries() : []) || {};
31 | this.fieldErrors = fieldErrors;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.github/workflows/pr.yml:
--------------------------------------------------------------------------------
1 | name: pr
2 | on: [pull_request]
3 | jobs:
4 | test:
5 | name: 'Test'
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/checkout@v3
9 | with:
10 | fetch-depth: '0'
11 | - uses: pnpm/action-setup@v2
12 | name: Install pnpm
13 | id: pnpm-install
14 | with:
15 | version: 7
16 | run_install: false
17 |
18 | - name: Get pnpm store directory
19 | id: pnpm-cache
20 | shell: bash
21 | run: |
22 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
23 |
24 | - uses: actions/cache@v3
25 | name: Setup pnpm cache
26 | with:
27 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
28 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
29 | restore-keys: |
30 | ${{ runner.os }}-pnpm-store-
31 |
32 | - name: Build & Test
33 | run: |
34 | pnpm install --no-frozen-lockfile
35 | pnpm build
36 | pnpm test
37 |
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "astro-bling",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "private": true,
6 | "scripts": {
7 | "dev": "astro dev",
8 | "start": "npm run dev",
9 | "build": "astro build",
10 | "preview": "astro preview",
11 | "astro": "astro"
12 | },
13 | "dependencies": {
14 | "@astrojs/netlify": "^1.2.1",
15 | "@astrojs/node": "^5.0.3",
16 | "@astrojs/react": "^1.2.2",
17 | "@babel/generator": "^7.21.1",
18 | "@prisma/client": "^4.11.0",
19 | "@remix-run/router": "^1.3.3",
20 | "@tanstack/bling": "^0.1.3",
21 | "@tanstack/react-query": "^4.26.0",
22 | "@vitejs/plugin-react": "^3.1.0",
23 | "astro": "0.0.0-ssr-manifest-20230306183729",
24 | "concurrently": "^7.6.0",
25 | "react": "^18.2.0",
26 | "react-dom": "^18.2.0",
27 | "react-router-dom": "^6.8.2",
28 | "tiny-invariant": "^1.3.1"
29 | },
30 | "devDependencies": {
31 | "@types/react": "^18.0.28",
32 | "@types/react-dom": "^18.0.11",
33 | "prisma": "^4.11.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/src/app/entry-server.tsx:
--------------------------------------------------------------------------------
1 | import { hasHandler, handleFetch$ } from '@tanstack/bling/server'
2 | import type { APIContext } from 'astro'
3 | import { renderToStringAsync } from 'solid-js/web'
4 | import { manifest } from 'astro:ssr-manifest'
5 | import { manifestContext } from './manifest'
6 | import { routes } from './root'
7 | import { Router, useRoutes } from '@solidjs/router'
8 |
9 | export const requestHandler = async ({ request }: APIContext) => {
10 | if (hasHandler(new URL(request.url).pathname)) {
11 | return await handleFetch$({
12 | request,
13 | })
14 | }
15 |
16 | return new Response(
17 | await renderToStringAsync(() => {
18 | const Routes = useRoutes(routes)
19 | return (
20 |
21 |
22 |
23 |
24 |
25 | )
26 | }),
27 | {
28 | headers: {
29 | 'content-type': 'text/html',
30 | },
31 | },
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/src/app/entry-server.tsx:
--------------------------------------------------------------------------------
1 | import { hasHandler, handleFetch$ } from '@tanstack/bling/server'
2 | import type { APIContext } from 'astro'
3 | import { renderToStringAsync } from 'solid-js/web'
4 | import { manifest } from 'astro:ssr-manifest'
5 | import { manifestContext } from './manifest'
6 | import { routes } from './root'
7 | import { Router, Routes, useRoutes } from '@solidjs/router'
8 |
9 | export const requestHandler = async ({ request }: APIContext) => {
10 | if (hasHandler(new URL(request.url).pathname)) {
11 | return await handleFetch$({
12 | request,
13 | })
14 | }
15 |
16 | return new Response(
17 | await renderToStringAsync(() => {
18 | const Routes = useRoutes(routes)
19 | return (
20 |
21 |
22 |
23 |
24 |
25 | )
26 | }),
27 | {
28 | headers: {
29 | 'content-type': 'text/html',
30 | },
31 | },
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/src/app/data/LICENSE.remix.md:
--------------------------------------------------------------------------------
1 | Copyright 2021 Remix Software Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Tanner Linsley
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "repository": "https://github.com/tanstack/bling.git",
4 | "scripts": {
5 | "build": "ts-node scripts/build.ts",
6 | "dev": "pnpm -rc --filter \"./packages/**\" --parallel exec 'pnpm dev'",
7 | "test": "exit 0",
8 | "test:dev": "exit 0",
9 | "test:ci": "exit 0",
10 | "clean-all": "pnpm -rc --parallel exec 'rm -rf build dist node_modules'",
11 | "prettier": "prettier \"packages/*/{src/**,examples/**/src/**}.{md,js,jsx,ts,tsx,json}\" --write",
12 | "pub": "pnpm build && pnpm publish -r --no-git-checks",
13 | "cipublish": "ts-node scripts/publish.ts",
14 | "cipublishforce": "CI=true pnpm cipublish"
15 | },
16 | "pnpm": {
17 | "overrides": {
18 | "@tanstack/bling": "workspace:*"
19 | }
20 | },
21 | "devDependencies": {
22 | "@commitlint/parse": "^17.4.4",
23 | "@types/node": "^18.14.0",
24 | "axios": "^1.3.4",
25 | "current-git-branch": "^1.1.0",
26 | "git-log-parser": "^1.2.0",
27 | "jsonfile": "^6.1.0",
28 | "luxon": "^3.2.1",
29 | "stream-to-array": "^2.3.0",
30 | "ts-node": "^10.9.1",
31 | "type-fest": "^3.6.0",
32 | "vite": "^4.1.4"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/bling/src/astro.ts:
--------------------------------------------------------------------------------
1 | import { bling } from './vite'
2 | import { fileURLToPath } from 'url'
3 | import type { AstroIntegration, AstroConfig } from 'astro'
4 | // https://astro.build/config
5 | export function astroBling(): AstroIntegration {
6 | let astroConfig: AstroConfig
7 | return {
8 | name: '',
9 |
10 | hooks: {
11 | 'astro:config:setup': (config) => {
12 | config.updateConfig({
13 | vite: {
14 | plugins: [bling()],
15 | },
16 | })
17 | },
18 | 'astro:config:done': (config) => {
19 | astroConfig = config.config
20 | },
21 | 'astro:build:ssr': (config) => {
22 | console.log(astroConfig)
23 | let entryClient = fileURLToPath(
24 | new URL('./src/app/entry-client.tsx', astroConfig.root),
25 | )
26 |
27 | ;(config.manifest as any)['entry-client'] =
28 | config.manifest.entryModules[entryClient]
29 | },
30 | 'astro:build:done': (config) => {},
31 | 'astro:build:setup': (config) => {
32 | if (config.target === 'client') {
33 | if (Array.isArray(config.vite.build?.rollupOptions?.input)) {
34 | config.vite.build?.rollupOptions?.input.push(
35 | 'src/app/entry-client.tsx',
36 | )
37 | }
38 |
39 | if (config.vite.build) {
40 | config.vite.build.ssrManifest = true
41 | config.vite.build.manifest = true
42 | }
43 | }
44 | },
45 | },
46 | } satisfies AstroIntegration
47 | }
48 |
--------------------------------------------------------------------------------
/examples/astro-react-router/src/app/entry-server.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | hasHandler,
3 | handleFetch$,
4 | addDeserializer,
5 | } from '@tanstack/bling/server'
6 | import type { APIContext } from 'astro'
7 | import * as ReactDOM from 'react-dom/server.browser'
8 | import { createStaticHandler } from '@remix-run/router'
9 | import {
10 | createStaticRouter,
11 | StaticRouterProvider,
12 | } from 'react-router-dom/server'
13 | import { routes } from './root'
14 | import { manifest } from 'astro:ssr-manifest'
15 | import { manifestContext } from './manifest'
16 |
17 | addDeserializer({
18 | apply: (req) => req === '$request',
19 | deserialize: (value, ctx) => ctx.request,
20 | })
21 |
22 | export const requestHandler = async ({ request }: APIContext) => {
23 | // manifest['entry-client'] = 1
24 | if (hasHandler(new URL(request.url).pathname)) {
25 | return await handleFetch$({
26 | request,
27 | })
28 | }
29 |
30 | let { query } = createStaticHandler(routes)
31 | let context = await query(request)
32 |
33 | if (context instanceof Response) {
34 | throw context
35 | }
36 |
37 | let router = createStaticRouter(routes, context)
38 |
39 | return new Response(
40 | await ReactDOM.renderToReadableStream(
41 |
42 |
47 | ,
48 | ),
49 | {
50 | headers: {
51 | 'content-type': 'text/html',
52 | },
53 | },
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/src/app/entry-server.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | hasHandler,
3 | handleFetch$,
4 | addDeserializer,
5 | } from '@tanstack/bling/server'
6 | import type { APIContext } from 'astro'
7 | import * as ReactDOM from 'react-dom/server.browser'
8 | import { createStaticHandler } from '@remix-run/router'
9 | import {
10 | createStaticRouter,
11 | StaticRouterProvider,
12 | } from 'react-router-dom/server'
13 | import { routes } from './root'
14 | import { manifestContext } from './manifest'
15 | import { manifest } from 'astro:ssr-manifest'
16 |
17 | addDeserializer({
18 | apply: (req) => req === '$request',
19 | deserialize: (value, ctx) => ctx.request,
20 | })
21 |
22 | export const requestHandler = async ({ request }: APIContext) => {
23 | // manifest['entry-client'] = 1
24 | if (hasHandler(new URL(request.url).pathname)) {
25 | return await handleFetch$({
26 | request,
27 | })
28 | }
29 |
30 | let { query } = createStaticHandler(routes)
31 | let context = await query(request)
32 |
33 | if (context instanceof Response) {
34 | throw context
35 | }
36 |
37 | let router = createStaticRouter(routes, context)
38 |
39 | return new Response(
40 | await ReactDOM.renderToReadableStream(
41 |
42 |
47 | ,
48 | ),
49 | {
50 | headers: {
51 | 'content-type': 'text/html',
52 | },
53 | },
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/src/app/components/story.tsx:
--------------------------------------------------------------------------------
1 | import { Component, Show } from 'solid-js'
2 | import { A } from '@solidjs/router'
3 |
4 | import type { IStory } from '../types'
5 |
6 | const Story: Component<{ story: IStory }> = (props) => {
7 | return (
8 |
9 | {props.story.points}
10 |
11 | {props.story.title}}
14 | >
15 |
16 | {props.story.title}
17 |
18 | ({props.story.domain})
19 |
20 |
21 |
22 |
23 | {props.story.time_ago}
27 | }
28 | >
29 | by {props.story.user}{' '}
30 | {props.story.time_ago} |{' '}
31 |
32 | {props.story.comments_count
33 | ? `${props.story.comments_count} comments`
34 | : 'discuss'}
35 |
36 |
37 |
38 |
39 | {' '}
40 | {props.story.type}
41 |
42 |
43 | )
44 | }
45 |
46 | export default Story
47 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 | concurrency:
3 | group: publish-${{ github.github.base_ref }}
4 | cancel-in-progress: true
5 | on: [push]
6 | jobs:
7 | test-and-publish:
8 | name: 'Test & Publish'
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 | with:
13 | fetch-depth: '0'
14 | - uses: pnpm/action-setup@v2
15 | name: Install pnpm
16 | id: pnpm-install
17 | with:
18 | version: 7
19 | run_install: false
20 | env:
21 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
22 |
23 | - name: Get pnpm store directory
24 | id: pnpm-cache
25 | shell: bash
26 | run: |
27 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
28 | env:
29 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
30 |
31 | - uses: actions/cache@v3
32 | name: Setup pnpm cache
33 | with:
34 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
35 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
36 | restore-keys: |
37 | ${{ runner.os }}-pnpm-store-
38 |
39 | - name: Publish
40 | run: |
41 | pnpm install --no-frozen-lockfile
42 | git config --global user.name 'Tanner Linsley'
43 | git config --global user.email 'tannerlinsley@users.noreply.github.com'
44 | pnpm cipublish
45 | env:
46 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
47 | GH_TOKEN: ${{ secrets.GH_TOKEN }}
48 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
49 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
50 |
--------------------------------------------------------------------------------
/examples/astro-solid/src/app/root.tsx:
--------------------------------------------------------------------------------
1 | import { server$, secret$, import$ } from '@tanstack/bling'
2 | import { createSignal, lazy, Suspense, useContext } from 'solid-js'
3 | import { HydrationScript, NoHydration } from 'solid-js/web'
4 | import { manifestContext } from './manifest'
5 | import { secret } from './server.secret$'
6 |
7 | const sayHello = server$(() => console.log('Hello world'))
8 |
9 | const LazyHello3 = lazy(() =>
10 | import$({
11 | default: () => {
12 | return (
13 | <>
14 |
15 | >
16 | )
17 | },
18 | }),
19 | )
20 |
21 | const inlineSecret = secret$('I am an inline server secret!')
22 |
23 | export function App() {
24 | console.log(
25 | 'Do you know the inline server secret?',
26 | inlineSecret ?? 'Not even.',
27 | )
28 |
29 | console.log(
30 | 'Do you know the server secret in server.secret.ts?',
31 | secret ?? 'Nope',
32 | )
33 |
34 | const [state, setState] = createSignal(0)
35 |
36 | return (
37 |
38 |
39 | Hello World
40 |
41 |
42 | Hello world
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | function Scripts() {
54 | const manifest = useContext(manifestContext)
55 | return (
56 |
57 |
58 | {import.meta.env.DEV ? (
59 | <>
60 |
61 |
66 | >
67 | ) : (
68 | <>
69 |
74 | >
75 | )}
76 |
77 | )
78 | }
79 |
--------------------------------------------------------------------------------
/examples/astro-react/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to [Astro](https://astro.build)
2 |
3 | [](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
4 | [](https://codesandbox.io/s/github/withastro/astro/tree/latest/examples/basics)
5 |
6 | > 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
7 |
8 | 
9 |
10 |
11 | ## 🚀 Project Structure
12 |
13 | Inside of your Astro project, you'll see the following folders and files:
14 |
15 | ```
16 | /
17 | ├── public/
18 | │ └── favicon.svg
19 | ├── src/
20 | │ ├── components/
21 | │ │ └── Card.astro
22 | │ ├── layouts/
23 | │ │ └── Layout.astro
24 | │ └── pages/
25 | │ └── index.astro
26 | └── package.json
27 | ```
28 |
29 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
30 |
31 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
32 |
33 | Any static assets, like images, can be placed in the `public/` directory.
34 |
35 | ## 🧞 Commands
36 |
37 | All commands are run from the root of the project, from a terminal:
38 |
39 | | Command | Action |
40 | | :--------------------- | :------------------------------------------------- |
41 | | `npm install` | Installs dependencies |
42 | | `npm run dev` | Starts local dev server at `localhost:3000` |
43 | | `npm run build` | Build your production site to `./dist/` |
44 | | `npm run preview` | Preview your build locally, before deploying |
45 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro preview` |
46 | | `npm run astro --help` | Get help using the Astro CLI |
47 |
48 | ## 👀 Want to learn more?
49 |
50 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
51 |
--------------------------------------------------------------------------------
/examples/astro-solid/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to [Astro](https://astro.build)
2 |
3 | [](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
4 | [](https://codesandbox.io/s/github/withastro/astro/tree/latest/examples/basics)
5 |
6 | > 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
7 |
8 | 
9 |
10 |
11 | ## 🚀 Project Structure
12 |
13 | Inside of your Astro project, you'll see the following folders and files:
14 |
15 | ```
16 | /
17 | ├── public/
18 | │ └── favicon.svg
19 | ├── src/
20 | │ ├── components/
21 | │ │ └── Card.astro
22 | │ ├── layouts/
23 | │ │ └── Layout.astro
24 | │ └── pages/
25 | │ └── index.astro
26 | └── package.json
27 | ```
28 |
29 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
30 |
31 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
32 |
33 | Any static assets, like images, can be placed in the `public/` directory.
34 |
35 | ## 🧞 Commands
36 |
37 | All commands are run from the root of the project, from a terminal:
38 |
39 | | Command | Action |
40 | | :--------------------- | :------------------------------------------------- |
41 | | `npm install` | Installs dependencies |
42 | | `npm run dev` | Starts local dev server at `localhost:3000` |
43 | | `npm run build` | Build your production site to `./dist/` |
44 | | `npm run preview` | Preview your build locally, before deploying |
45 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro preview` |
46 | | `npm run astro --help` | Get help using the Astro CLI |
47 |
48 | ## 👀 Want to learn more?
49 |
50 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
51 |
--------------------------------------------------------------------------------
/examples/astro-react-router/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to [Astro](https://astro.build)
2 |
3 | [](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
4 | [](https://codesandbox.io/s/github/withastro/astro/tree/latest/examples/basics)
5 |
6 | > 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
7 |
8 | 
9 |
10 |
11 | ## 🚀 Project Structure
12 |
13 | Inside of your Astro project, you'll see the following folders and files:
14 |
15 | ```
16 | /
17 | ├── public/
18 | │ └── favicon.svg
19 | ├── src/
20 | │ ├── components/
21 | │ │ └── Card.astro
22 | │ ├── layouts/
23 | │ │ └── Layout.astro
24 | │ └── pages/
25 | │ └── index.astro
26 | └── package.json
27 | ```
28 |
29 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
30 |
31 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
32 |
33 | Any static assets, like images, can be placed in the `public/` directory.
34 |
35 | ## 🧞 Commands
36 |
37 | All commands are run from the root of the project, from a terminal:
38 |
39 | | Command | Action |
40 | | :--------------------- | :------------------------------------------------- |
41 | | `npm install` | Installs dependencies |
42 | | `npm run dev` | Starts local dev server at `localhost:3000` |
43 | | `npm run build` | Build your production site to `./dist/` |
44 | | `npm run preview` | Preview your build locally, before deploying |
45 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro preview` |
46 | | `npm run astro --help` | Get help using the Astro CLI |
47 |
48 | ## 👀 Want to learn more?
49 |
50 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
51 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to [Astro](https://astro.build)
2 |
3 | [](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
4 | [](https://codesandbox.io/s/github/withastro/astro/tree/latest/examples/basics)
5 |
6 | > 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
7 |
8 | 
9 |
10 |
11 | ## 🚀 Project Structure
12 |
13 | Inside of your Astro project, you'll see the following folders and files:
14 |
15 | ```
16 | /
17 | ├── public/
18 | │ └── favicon.svg
19 | ├── src/
20 | │ ├── components/
21 | │ │ └── Card.astro
22 | │ ├── layouts/
23 | │ │ └── Layout.astro
24 | │ └── pages/
25 | │ └── index.astro
26 | └── package.json
27 | ```
28 |
29 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
30 |
31 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
32 |
33 | Any static assets, like images, can be placed in the `public/` directory.
34 |
35 | ## 🧞 Commands
36 |
37 | All commands are run from the root of the project, from a terminal:
38 |
39 | | Command | Action |
40 | | :--------------------- | :------------------------------------------------- |
41 | | `npm install` | Installs dependencies |
42 | | `npm run dev` | Starts local dev server at `localhost:3000` |
43 | | `npm run build` | Build your production site to `./dist/` |
44 | | `npm run preview` | Preview your build locally, before deploying |
45 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro preview` |
46 | | `npm run astro --help` | Get help using the Astro CLI |
47 |
48 | ## 👀 Want to learn more?
49 |
50 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
51 |
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to [Astro](https://astro.build)
2 |
3 | [](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
4 | [](https://codesandbox.io/s/github/withastro/astro/tree/latest/examples/basics)
5 |
6 | > 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
7 |
8 | 
9 |
10 |
11 | ## 🚀 Project Structure
12 |
13 | Inside of your Astro project, you'll see the following folders and files:
14 |
15 | ```
16 | /
17 | ├── public/
18 | │ └── favicon.svg
19 | ├── src/
20 | │ ├── components/
21 | │ │ └── Card.astro
22 | │ ├── layouts/
23 | │ │ └── Layout.astro
24 | │ └── pages/
25 | │ └── index.astro
26 | └── package.json
27 | ```
28 |
29 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
30 |
31 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
32 |
33 | Any static assets, like images, can be placed in the `public/` directory.
34 |
35 | ## 🧞 Commands
36 |
37 | All commands are run from the root of the project, from a terminal:
38 |
39 | | Command | Action |
40 | | :--------------------- | :------------------------------------------------- |
41 | | `npm install` | Installs dependencies |
42 | | `npm run dev` | Starts local dev server at `localhost:3000` |
43 | | `npm run build` | Build your production site to `./dist/` |
44 | | `npm run preview` | Preview your build locally, before deploying |
45 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro preview` |
46 | | `npm run astro --help` | Get help using the Astro CLI |
47 |
48 | ## 👀 Want to learn more?
49 |
50 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
51 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to [Astro](https://astro.build)
2 |
3 | [](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
4 | [](https://codesandbox.io/s/github/withastro/astro/tree/latest/examples/basics)
5 |
6 | > 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
7 |
8 | 
9 |
10 |
11 | ## 🚀 Project Structure
12 |
13 | Inside of your Astro project, you'll see the following folders and files:
14 |
15 | ```
16 | /
17 | ├── public/
18 | │ └── favicon.svg
19 | ├── src/
20 | │ ├── components/
21 | │ │ └── Card.astro
22 | │ ├── layouts/
23 | │ │ └── Layout.astro
24 | │ └── pages/
25 | │ └── index.astro
26 | └── package.json
27 | ```
28 |
29 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
30 |
31 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
32 |
33 | Any static assets, like images, can be placed in the `public/` directory.
34 |
35 | ## 🧞 Commands
36 |
37 | All commands are run from the root of the project, from a terminal:
38 |
39 | | Command | Action |
40 | | :--------------------- | :------------------------------------------------- |
41 | | `npm install` | Installs dependencies |
42 | | `npm run dev` | Starts local dev server at `localhost:3000` |
43 | | `npm run build` | Build your production site to `./dist/` |
44 | | `npm run preview` | Preview your build locally, before deploying |
45 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro preview` |
46 | | `npm run astro --help` | Get help using the Astro CLI |
47 |
48 | ## 👀 Want to learn more?
49 |
50 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
51 |
--------------------------------------------------------------------------------
/examples/astro-react/src/app/root.tsx:
--------------------------------------------------------------------------------
1 | import { server$, lazy$, import$, secret$ } from '@tanstack/bling'
2 | import React, { Fragment, lazy, Suspense, useContext } from 'react'
3 | import { manifestContext } from './manifest'
4 |
5 | const sayHello = server$(() => console.log('Hello world'))
6 |
7 | function ServerHello() {
8 | return
9 | }
10 |
11 | const LazyHello = lazy$((props) => {
12 | return
13 | })
14 |
15 | const LazyHello2 = lazy$(function SplitHelloas() {
16 | return (
17 |
18 |
19 | sayHello()} />
20 |
21 | )
22 | })
23 |
24 | const LazyHello3 = lazy(() =>
25 | import$({
26 | default: () => {
27 | return (
28 |
29 |
30 | sayHello()} />
31 |
32 | )
33 | },
34 | }),
35 | )
36 |
37 | const inlineSecret = secret$('I am an inline server secret!')
38 |
39 | export function App() {
40 | console.log(
41 | 'Do you know the inline server secret?',
42 | inlineSecret ?? 'Not even.',
43 | )
44 |
45 | const [state, setState] = React.useState(0)
46 |
47 | return (
48 |
49 |
50 | Hello World
51 |
52 |
53 | Hello world
54 |
55 |
56 | {state > 0 && (
57 |
58 |
59 |
60 |
61 |
62 | )}
63 |
64 |
65 |
66 | )
67 | }
68 |
69 | function Scripts() {
70 | const manifest = useContext(manifestContext)
71 | return import.meta.env.DEV ? (
72 | <>
73 |
74 |
75 | >
76 | ) : (
77 | <>
78 |
79 | >
80 | )
81 | }
82 |
--------------------------------------------------------------------------------
/packages/bling/src/types.ts:
--------------------------------------------------------------------------------
1 | export const FormError = Error
2 | export const ServerError = Error
3 |
4 | export type Serializer = {
5 | apply: (value: any) => boolean
6 | serialize: (value: any) => any
7 | }
8 |
9 | export type Deserializer = {
10 | apply: (value: any) => any
11 | deserialize: (value: any, ctx: FetchFnCtx) => any
12 | }
13 |
14 | export type AnyFetchFn = (payload: any, ctx: FetchFnCtxWithRequest) => any
15 |
16 | export type FetchFnReturn = Awaited<
17 | ReturnType
18 | > extends JsonResponse
19 | ? R
20 | : ReturnType
21 |
22 | export type CreateFetcherFn = (
23 | fn: T,
24 | opts?: FetchFnCtxWithRequest,
25 | ) => Fetcher
26 |
27 | export type FetcherFn = (
28 | payload: Parameters['0'] extends undefined
29 | ? void | undefined
30 | : Parameters['0'],
31 | opts?: FetchFnCtx,
32 | ) => Promise>>
33 |
34 | export type FetcherMethods = {
35 | url: string
36 | fetch: (
37 | init: RequestInit,
38 | opts?: FetchFnCtxOptions,
39 | ) => Promise>>
40 | }
41 |
42 | export type Fetcher = FetcherFn & FetcherMethods
43 |
44 | export interface JsonResponse extends Response {}
45 |
46 | export type FetchFnCtxBase = {
47 | method?: 'GET' | 'POST'
48 | }
49 |
50 | export type FetchFnCtxOptions = FetchFnCtxBase & {
51 | request?: RequestInit
52 | __hasRequest?: never
53 | }
54 |
55 | export type FetchFnCtxWithRequest = FetchFnCtxBase & {
56 | request: Request
57 | __hasRequest: true
58 | }
59 |
60 | export type FetchFnCtx = FetchFnCtxOptions | FetchFnCtxWithRequest
61 |
62 | export type NonFnProps = {
63 | [TKey in keyof T]: TKey extends (...args: any[]) => any ? never : T[TKey]
64 | }
65 |
66 | export type AnySplitFn = (...args: any[]) => any
67 | export type ModuleObj = any
68 |
69 | export type CreateSplitFn = (fn: T) => SplitFn
70 |
71 | export type CreateLazyFn = (fn: T) => T
72 |
73 | export type CreateImportFn = (fn: T) => Promise
74 |
75 | export type SplitFn = (
76 | ...args: Parameters
77 | ) => Promise>>
78 |
79 | export type CreateSecretFn = (value: T) => T
80 |
--------------------------------------------------------------------------------
/packages/bling/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tanstack/bling",
3 | "version": "0.5.0",
4 | "description": "",
5 | "author": "Nikhil Saraf",
6 | "license": "MIT",
7 | "type": "module",
8 | "publishConfig": {
9 | "access": "public"
10 | },
11 | "files": [
12 | "*"
13 | ],
14 | "scripts": {
15 | "dev": "concurrently --kill-others \"pnpm build:types --watch\" \"pnpm build:server --watch\" \"pnpm build:vite --watch\" \"pnpm build:compilers --watch\" \"pnpm build:client --watch\" \"pnpm build:astro --watch\"",
16 | "build": "pnpm build:types && pnpm build:server && pnpm build:vite && pnpm build:astro && pnpm build:compilers && pnpm build:client",
17 | "build:types": "tsc --emitDeclarationOnly --declaration --skipLibCheck",
18 | "build:server": "esbuild src/server.ts --bundle --platform=node --format=esm --sourcemap --packages=external --outfile=dist/server.js",
19 | "build:vite": "esbuild src/vite.ts --bundle --platform=node --format=esm --sourcemap --packages=external --outfile=dist/vite.js",
20 | "build:astro": "esbuild src/astro.ts --bundle --platform=node --format=esm --sourcemap --packages=external --outfile=dist/astro.js",
21 | "build:compilers": "esbuild src/compilers.ts --bundle --platform=node --format=esm --sourcemap --packages=external --outfile=dist/compilers.js",
22 | "build:client": "esbuild src/client.ts --bundle --format=esm --minify --sourcemap --outfile=dist/client.js",
23 | "prettier": "prettier \"packages/*/{src/**,examples/**/src/**}.{md,js,jsx,ts,tsx,json}\" --write"
24 | },
25 | "exports": {
26 | ".": "./dist/client.js",
27 | "./server": "./dist/server.js",
28 | "./client": "./dist/client.js",
29 | "./vite": "./dist/vite.js",
30 | "./astro": "./dist/astro.js",
31 | "./compilers": "./dist/compilers.js",
32 | "./package.json": "./package.json"
33 | },
34 | "types": "./dist/server.d.ts",
35 | "typesVersions": {
36 | "*": {
37 | ".": [
38 | "./dist/client.d.ts"
39 | ],
40 | "client": [
41 | "./dist/client.d.ts"
42 | ],
43 | "server": [
44 | "./dist/server.d.ts"
45 | ],
46 | "vite": [
47 | "./dist/vite.d.ts"
48 | ],
49 | "astro": [
50 | "./dist/astro.d.ts"
51 | ],
52 | "compilers": [
53 | "./dist/compilers.d.ts"
54 | ]
55 | }
56 | },
57 | "dependencies": {
58 | "@babel/generator": "^7.21.1",
59 | "@babel/template": "^7.20.7",
60 | "@babel/traverse": "^7.21.2",
61 | "@babel/types": "^7.21.2",
62 | "@vitejs/plugin-react": "^3.1.0",
63 | "esbuild": "^0.16.17",
64 | "esbuild-plugin-replace": "^1.3.0"
65 | },
66 | "devDependencies": {
67 | "@types/babel__core": "^7.20.0",
68 | "@types/babel__generator": "^7.6.4",
69 | "@types/babel__template": "^7.4.1",
70 | "@types/babel__traverse": "^7.18.3",
71 | "astro": "0.0.0-ssr-manifest-20230306183729",
72 | "concurrently": "^7.6.0",
73 | "typescript": "4.9.4",
74 | "vitest": "^0.26.2"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/packages/bling/src/vite.ts:
--------------------------------------------------------------------------------
1 | import type { Plugin } from 'vite'
2 | import viteReact, { Options } from '@vitejs/plugin-react'
3 | import { fileURLToPath, pathToFileURL } from 'url'
4 | import { compileSecretFile, compileFile, splitFile } from './compilers'
5 |
6 | export const virtualModuleSplitPrefix = 'virtual:bling-split$-'
7 | export const virtualPrefix = '\0'
8 |
9 | export function bling(opts?: { babel?: Options['babel'] }): Plugin {
10 | const options = opts ?? {}
11 |
12 | return {
13 | name: 'vite-plugin-bling',
14 | enforce: 'pre',
15 | transform: async (code, id, transformOptions) => {
16 | const isSsr =
17 | transformOptions === null || transformOptions === void 0
18 | ? void 0
19 | : transformOptions.ssr
20 |
21 | let ssr = process.env.TEST_ENV === 'client' ? false : !!isSsr
22 |
23 | let [fileId, queryParam] = id.split('?')
24 |
25 | let param = new URLSearchParams(queryParam)
26 |
27 | const url = pathToFileURL(id)
28 | url.searchParams.delete('v')
29 | id = fileURLToPath(url).replace(/\\/g, '/')
30 |
31 | const babelOptions =
32 | (fn?: (source: any, id: any) => { plugins: any[] }) =>
33 | (source: any, id: any) => {
34 | const b: any =
35 | typeof options.babel === 'function'
36 | ? // @ts-ignore
37 | options.babel(...args)
38 | : options.babel ?? { plugins: [] }
39 | const d = fn?.(source, id)
40 | return {
41 | plugins: [...b.plugins, ...(d?.plugins ?? [])],
42 | }
43 | }
44 |
45 | let viteCompile = (
46 | code: string,
47 | id: string,
48 | fn?: (source: any, id: any) => { plugins: any[] },
49 | ) => {
50 | let plugin = viteReact({
51 | ...(options ?? {}),
52 | fastRefresh: false,
53 | babel: babelOptions(fn),
54 | })
55 |
56 | // @ts-ignore
57 | return plugin[0].transform(code, id, transformOptions)
58 | }
59 |
60 | if (param.has('split')) {
61 | const compiled = await splitFile({
62 | code,
63 | viteCompile,
64 | ssr,
65 | id: fileId.replace(/\.ts$/, '.tsx').replace(/\.js$/, '.jsx'),
66 | splitIndex: Number(param.get('split')),
67 | ref: param.get('ref') ?? 'fn',
68 | })
69 |
70 | return compiled.code
71 | }
72 |
73 | if (url.pathname.includes('.secret$.') && !ssr) {
74 | const compiled = compileSecretFile({
75 | code,
76 | })
77 |
78 | return compiled.code
79 | }
80 |
81 | if (
82 | code.includes('fetch$(' || code.includes('split$(')) ||
83 | code.includes('server$(')
84 | ) {
85 | const compiled = await compileFile({
86 | code,
87 | viteCompile,
88 | ssr,
89 | id: id.replace(/\.ts$/, '.tsx').replace(/\.js$/, '.jsx'),
90 | })
91 |
92 | return compiled.code
93 | }
94 | },
95 | }
96 | }
97 |
98 | export default bling
99 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/src/app/root.tsx:
--------------------------------------------------------------------------------
1 | import { server$, import$, secret$, redirect } from '@tanstack/bling'
2 | import { createSignal, lazy, Show, Suspense, useContext } from 'solid-js'
3 | import { HydrationScript, NoHydration } from 'solid-js/web'
4 | import { manifestContext } from './manifest'
5 | import { Link, Outlet, RouteDefinition, useRouteData } from '@solidjs/router'
6 | import { useAction, useLoader } from './data'
7 |
8 | const sayHello = server$(() => console.log('Hello world'))
9 |
10 | const LazyHello3 = lazy(() =>
11 | import$({
12 | default: () => {
13 | return (
14 | <>
15 |
16 | >
17 | )
18 | },
19 | }),
20 | )
21 |
22 | const inlineSecret = secret$('I am an inline server secret!')
23 |
24 | export function App() {
25 | console.log(
26 | 'Do you know the inline server secret?',
27 | inlineSecret ?? 'Not even.',
28 | )
29 |
30 | const [state, setState] = createSignal(0)
31 |
32 | return (
33 |
34 |
35 | Hello World
36 |
37 |
38 | Hello world
39 | Home
40 | About
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | )
49 | }
50 |
51 | function Scripts() {
52 | const manifest = useContext(manifestContext)
53 | return (
54 |
55 |
56 | {import.meta.env.DEV ? (
57 | <>
58 |
59 |
64 | >
65 | ) : (
66 | <>
67 |
72 | >
73 | )}
74 |
75 | )
76 | }
77 |
78 | let count = 0
79 |
80 | const increment = server$(async () => {
81 | count = count + 1
82 | return redirect('/about')
83 | })
84 |
85 | export const routes = [
86 | {
87 | path: '/',
88 | component: App,
89 | children: [
90 | {
91 | path: '',
92 | component: lazy(() => import$({ default: () => Home
})),
93 | },
94 | {
95 | path: 'about',
96 | data: () => {
97 | return useLoader(server$(() => ({ count })))
98 | },
99 | component: lazy(() =>
100 | import$({
101 | default: () => {
102 | const routeData = useRouteData()
103 | const [action, submit] = useAction(increment)
104 | return (
105 |
106 | About {routeData().count}
107 |
108 |
109 |
110 |
111 |
112 | )
113 | },
114 | }),
115 | ),
116 | },
117 | ],
118 | },
119 | ] satisfies RouteDefinition[]
120 |
--------------------------------------------------------------------------------
/examples/astro-react-router/src/app/root.tsx:
--------------------------------------------------------------------------------
1 | import { server$, lazy$, secret$, import$ } from '@tanstack/bling'
2 | import React, { Fragment, lazy, Suspense, useContext } from 'react'
3 | import { Link, Outlet, RouteObject, useLoaderData } from 'react-router-dom'
4 | import { manifestContext } from './manifest'
5 |
6 | const sayHello = server$(() => console.log('Hello world'))
7 |
8 | function ServerHello() {
9 | return
10 | }
11 |
12 | const LazyHello = lazy$((props) => {
13 | return
14 | })
15 |
16 | const LazyHello2 = lazy$(function SplitHelloas() {
17 | return (
18 |
19 |
20 | sayHello()} />
21 |
22 | )
23 | })
24 |
25 | const LazyHello3 = lazy(() =>
26 | import$({
27 | default: () => {
28 | return (
29 |
30 |
31 | sayHello()} />
32 |
33 | )
34 | },
35 | }),
36 | )
37 |
38 | const inlineSecret = secret$('I am an inline server secret!')
39 |
40 | const App = lazy$(() => {
41 | console.log(
42 | 'Do you know the inline server secret?',
43 | inlineSecret ?? 'Not even.',
44 | )
45 |
46 | return (
47 |
48 |
49 | Hello World
50 |
51 |
52 | Hello world
53 | hello
54 | home
55 |
56 |
57 |
58 |
59 |
60 |
61 | )
62 | })
63 |
64 | const SomeRoute = lazy$(() => {
65 | const [state, setState] = React.useState(0)
66 |
67 | return (
68 | <>
69 |
70 |
71 | {state > 0 && (
72 |
73 |
74 |
75 |
76 |
77 | )}
78 | >
79 | )
80 | })
81 |
82 | const SomeRoute2 = lazy(() =>
83 | import$({
84 | default: () => {
85 | const [state, setState] = React.useState(0)
86 | const data = useLoaderData()
87 | console.log(data)
88 |
89 | return (
90 | <>
91 |
92 | >
93 | )
94 | },
95 | }),
96 | )
97 |
98 | function Scripts() {
99 | const manifest = useContext(manifestContext)
100 | return import.meta.env.DEV ? (
101 | <>
102 |
103 |
104 | >
105 | ) : (
106 |
107 | )
108 | }
109 |
110 | export let routes = [
111 | {
112 | path: '/',
113 | element: ,
114 | children: [
115 | {
116 | index: true,
117 | element: ,
118 | },
119 | {
120 | path: 'hello',
121 | loader: server$((args, { request }) => {
122 | return {
123 | 'got data': inlineSecret,
124 | req: [...request.headers.entries()],
125 | }
126 | }),
127 |
128 | element: ,
129 | },
130 | ],
131 | },
132 | ] satisfies RouteObject[]
133 |
--------------------------------------------------------------------------------
/packages/bling/src/client.ts:
--------------------------------------------------------------------------------
1 | import {
2 | mergeRequestInits,
3 | mergeFetchOpts,
4 | parseResponse,
5 | payloadRequestInit,
6 | resolveRequestHref,
7 | XBlingOrigin,
8 | XBlingResponseTypeHeader,
9 | } from './utils/utils'
10 |
11 | import type {
12 | AnyFetchFn,
13 | Serializer,
14 | FetcherFn,
15 | FetcherMethods,
16 | FetchFnReturn,
17 | FetchFnCtxOptions,
18 | FetchFnCtx,
19 | CreateSplitFn,
20 | CreateSecretFn,
21 | CreateImportFn,
22 | FetchFnCtxWithRequest,
23 | } from './types'
24 |
25 | export * from './utils/utils'
26 |
27 | //
28 |
29 | let serializers: Serializer[] = []
30 |
31 | export function addSerializer({ apply, serialize }: Serializer) {
32 | serializers.push({ apply, serialize })
33 | }
34 |
35 | export type CreateClientFetcherFn = (
36 | fn: T,
37 | opts?: FetchFnCtxWithRequest,
38 | ) => ClientFetcher
39 |
40 | export type CreateClientFetcherMethods = {
41 | createFetcher(
42 | route: string,
43 | defualtOpts: FetchFnCtxOptions,
44 | ): ClientFetcher
45 | }
46 |
47 | export type ClientFetcher = FetcherFn &
48 | FetcherMethods
49 |
50 | export type ClientFetcherMethods = FetcherMethods & {
51 | fetch: (
52 | init: RequestInit,
53 | opts?: FetchFnCtxOptions,
54 | ) => Promise>>
55 | }
56 |
57 | export type ClientFetchFn = CreateClientFetcherFn & CreateClientFetcherMethods
58 |
59 | const fetchImpl = (() => {
60 | throw new Error('Should be compiled away')
61 | }) as any
62 |
63 | const fetchMethods: CreateClientFetcherMethods = {
64 | createFetcher: (pathname: string, defaultOpts?: FetchFnCtxOptions) => {
65 | const fetcherImpl = async (payload: any = {}, opts?: FetchFnCtxOptions) => {
66 | const method = opts?.method || defaultOpts?.method || 'POST'
67 |
68 | const baseInit: RequestInit = {
69 | method,
70 | headers: {
71 | [XBlingOrigin]: 'client',
72 | },
73 | }
74 |
75 | let payloadInit = payloadRequestInit(payload, serializers)
76 |
77 | const resolvedHref = resolveRequestHref(pathname, method, payloadInit)
78 |
79 | const requestInit = mergeRequestInits(
80 | baseInit,
81 | payloadInit,
82 | defaultOpts?.request,
83 | opts?.request,
84 | )
85 | const request = new Request(resolvedHref, requestInit)
86 |
87 | const response = await fetch(request)
88 |
89 | // // throws response, error, form error, json object, string
90 | if (response.headers.get(XBlingResponseTypeHeader) === 'throw') {
91 | throw await parseResponse(response)
92 | } else {
93 | return await parseResponse(response)
94 | }
95 | }
96 |
97 | const fetcherMethods: ClientFetcherMethods = {
98 | url: pathname,
99 | fetch: (request: RequestInit, opts?: FetchFnCtxOptions) => {
100 | return fetcherImpl({}, mergeFetchOpts({ request }, opts) as any)
101 | },
102 | }
103 |
104 | return Object.assign(fetcherImpl, fetcherMethods) as ClientFetcher
105 | },
106 | }
107 |
108 | export const fetch$: ClientFetchFn = Object.assign(fetchImpl, fetchMethods)
109 | export const server$: ClientFetchFn = fetch$
110 |
111 | export const split$: CreateSplitFn = (_fn) => {
112 | throw new Error('Should be compiled away')
113 | }
114 |
115 | export const import$: CreateImportFn = (_fn) => {
116 | throw new Error('Should be compiled away')
117 | }
118 |
119 | export const lazy$: CreateSplitFn = (_fn) => {
120 | throw new Error('Should be compiled away')
121 | }
122 |
123 | export const secret$: CreateSecretFn = (_value) => {
124 | throw new Error('Should be compiled away')
125 | }
126 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: '🐛 Bug report'
2 | description: Create a report to help us improve
3 | body:
4 | - type: markdown
5 | attributes:
6 | value: |
7 | Thank you for reporting an issue :pray:.
8 |
9 | This issue tracker is for reporting bugs found in `router` (https://github.com/tanstack/router).
10 | If you have a question about how to achieve something and are struggling, please post a question
11 | inside of `router` Discussions tab: https://github.com/tanstack/router/discussions
12 |
13 | Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already:
14 | - `router` Issues tab: https://github.com/tanstack/router/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc
15 | - `router` closed issues tab: https://github.com/tanstack/router/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed
16 | - `router` Discussions tab: https://github.com/tanstack/router/discussions
17 |
18 | The more information you fill in, the better the community can help you.
19 | - type: textarea
20 | id: description
21 | attributes:
22 | label: Describe the bug
23 | description: Provide a clear and concise description of the challenge you are running into.
24 | validations:
25 | required: true
26 | - type: input
27 | id: link
28 | attributes:
29 | label: Your Example Website or App
30 | description: |
31 | Which website or app were you using when the bug happened?
32 | Note:
33 | - Please provide a link via our pre-configured [Stackblitz project](https://stackblitz.com/github/tanstack/router/tree/beta/examples/react/quickstart?file=src%2Fmain.tsx) or a link to a repo that can reproduce the issue.
34 | - Your bug will may get fixed much faster if we can run your code and it doesn't have dependencies other than the `router` npm package / dependency.
35 | - To create a shareable code example you can use Stackblitz. Please no localhost URLs.
36 | - Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve.
37 | placeholder: reproduction URL
38 | validations:
39 | required: true
40 | - type: textarea
41 | id: steps
42 | attributes:
43 | label: Steps to Reproduce the Bug or Issue
44 | description: Describe the steps we have to take to reproduce the behavior.
45 | placeholder: |
46 | 1. Go to '...'
47 | 2. Click on '....'
48 | 3. Scroll down to '....'
49 | 4. See error
50 | validations:
51 | required: true
52 | - type: textarea
53 | id: expected
54 | attributes:
55 | label: Expected behavior
56 | description: Provide a clear and concise description of what you expected to happen.
57 | placeholder: |
58 | As a user, I expected ___ behavior but i am seeing ___
59 | validations:
60 | required: true
61 | - type: textarea
62 | id: screenshots_or_videos
63 | attributes:
64 | label: Screenshots or Videos
65 | description: |
66 | If applicable, add screenshots or a video to help explain your problem.
67 | For more information on the supported file image/file types and the file size limits, please refer
68 | to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files
69 | placeholder: |
70 | You can drag your video or image files inside of this editor ↓
71 | - type: textarea
72 | id: platform
73 | attributes:
74 | label: Platform
75 | value: |
76 | - OS: [e.g. macOS, Windows, Linux]
77 | - Browser: [e.g. Chrome, Safari, Firefox]
78 | - Version: [e.g. 91.1]
79 | validations:
80 | required: true
81 | - type: textarea
82 | id: additional
83 | attributes:
84 | label: Additional context
85 | description: Add any other context about the problem here.
86 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/src/app/root.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
3 | font-size: 15px;
4 | background-color: #f2f3f5;
5 | margin: 0;
6 | padding-top: 55px;
7 | color: #34495e;
8 | overflow-y: scroll;
9 | }
10 |
11 | a {
12 | color: #34495e;
13 | text-decoration: none;
14 | }
15 |
16 | .header {
17 | background-color: #335d92;
18 | position: fixed;
19 | z-index: 999;
20 | height: 55px;
21 | top: 0;
22 | left: 0;
23 | right: 0;
24 | }
25 |
26 | .header .inner {
27 | max-width: 800px;
28 | box-sizing: border-box;
29 | margin: 0 auto;
30 | padding: 15px 5px;
31 | }
32 |
33 | .header a {
34 | color: rgba(255, 255, 255, 0.8);
35 | line-height: 24px;
36 | transition: color 0.15s ease;
37 | display: inline-block;
38 | vertical-align: middle;
39 | font-weight: 300;
40 | letter-spacing: 0.075em;
41 | margin-right: 1.8em;
42 | }
43 |
44 | .header a:hover {
45 | color: #fff;
46 | }
47 |
48 | .header a.active {
49 | color: #fff;
50 | font-weight: 400;
51 | }
52 |
53 | .header a:nth-child(6) {
54 | margin-right: 0;
55 | }
56 |
57 | .header .github {
58 | color: #fff;
59 | font-size: 0.9em;
60 | margin: 0;
61 | float: right;
62 | }
63 |
64 | .logo {
65 | width: 24px;
66 | margin-right: 10px;
67 | display: inline-block;
68 | vertical-align: middle;
69 | }
70 |
71 | .view {
72 | max-width: 800px;
73 | margin: 0 auto;
74 | position: relative;
75 | }
76 |
77 | @media (max-width: 860px) {
78 | .header .inner {
79 | padding: 15px 30px;
80 | }
81 | }
82 |
83 | @media (max-width: 600px) {
84 | .header .inner {
85 | padding: 15px;
86 | }
87 |
88 | .header a {
89 | margin-right: 1em;
90 | }
91 |
92 | .header .github {
93 | display: none;
94 | }
95 | }
96 |
97 | .news-view {
98 | padding-top: 45px;
99 | }
100 |
101 | .news-list, .news-list-nav {
102 | background-color: #fff;
103 | border-radius: 2px;
104 | }
105 |
106 | .news-list-nav {
107 | padding: 15px 30px;
108 | position: fixed;
109 | text-align: center;
110 | top: 55px;
111 | left: 0;
112 | right: 0;
113 | z-index: 998;
114 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
115 | }
116 |
117 | .news-list-nav .page-link {
118 | margin: 0 1em;
119 | }
120 |
121 | .news-list-nav .disabled {
122 | color: #aaa;
123 | }
124 |
125 | .news-list {
126 | position: absolute;
127 | margin: 30px 0;
128 | width: 100%;
129 | }
130 |
131 | .news-list ul {
132 | list-style-type: none;
133 | padding: 0;
134 | margin: 0;
135 | }
136 |
137 | @media (max-width: 600px) {
138 | .news-list {
139 | margin: 10px 0;
140 | }
141 | }
142 |
143 | .news-item {
144 | background-color: #fff;
145 | padding: 20px 30px 20px 80px;
146 | border-bottom: 1px solid #eee;
147 | position: relative;
148 | line-height: 20px;
149 | }
150 |
151 | .news-item .score {
152 | color: #335d92;
153 | font-size: 1.1em;
154 | font-weight: 700;
155 | position: absolute;
156 | top: 50%;
157 | left: 0;
158 | width: 80px;
159 | text-align: center;
160 | margin-top: -10px;
161 | }
162 |
163 | .news-item .host, .news-item .meta {
164 | font-size: 0.85em;
165 | color: #626262;
166 | }
167 |
168 | .news-item .host a, .news-item .meta a {
169 | color: #626262;
170 | text-decoration: underline;
171 | }
172 |
173 | .news-item .host a:hover, .news-item .meta a:hover {
174 | color: #335d92;
175 | }
176 |
177 | .item-view-header {
178 | background-color: #fff;
179 | padding: 1.8em 2em 1em;
180 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
181 | }
182 |
183 | .item-view-header h1 {
184 | display: inline;
185 | font-size: 1.5em;
186 | margin: 0;
187 | margin-right: 0.5em;
188 | }
189 |
190 | .item-view-header .host, .item-view-header .meta, .item-view-header .meta a {
191 | color: #626262;
192 | }
193 |
194 | .item-view-header .meta a {
195 | text-decoration: underline;
196 | }
197 |
198 | .item-view-comments {
199 | background-color: #fff;
200 | margin-top: 10px;
201 | padding: 0 2em 0.5em;
202 | }
203 |
204 | .item-view-comments-header {
205 | margin: 0;
206 | font-size: 1.1em;
207 | padding: 1em 0;
208 | position: relative;
209 | }
210 |
211 | .item-view-comments-header .spinner {
212 | display: inline-block;
213 | margin: -15px 0;
214 | }
215 |
216 | .comment-children {
217 | list-style-type: none;
218 | padding: 0;
219 | margin: 0;
220 | }
221 |
222 | @media (max-width: 600px) {
223 | .item-view-header h1 {
224 | font-size: 1.25em;
225 | }
226 | }
227 |
228 | .comment-children .comment-children {
229 | margin-left: 1.5em;
230 | }
231 |
232 | .comment {
233 | border-top: 1px solid #eee;
234 | position: relative;
235 | }
236 |
237 | .comment .by, .comment .text, .comment .toggle {
238 | font-size: 0.9em;
239 | margin: 1em 0;
240 | }
241 |
242 | .comment .by {
243 | color: #626262;
244 | }
245 |
246 | .comment .by a {
247 | color: #626262;
248 | text-decoration: underline;
249 | }
250 |
251 | .comment .text {
252 | overflow-wrap: break-word;
253 | }
254 |
255 | .comment .text a:hover {
256 | color: #335d92;
257 | }
258 |
259 | .comment .text pre {
260 | white-space: pre-wrap;
261 | }
262 |
263 | .comment .toggle {
264 | background-color: #fffbf2;
265 | padding: 0.3em 0.5em;
266 | border-radius: 4px;
267 | }
268 |
269 | .comment .toggle a {
270 | color: #626262;
271 | cursor: pointer;
272 | }
273 |
274 | .comment .toggle.open {
275 | padding: 0;
276 | background-color: transparent;
277 | margin-bottom: -0.5em;
278 | }
279 |
280 | .user-view {
281 | background-color: #fff;
282 | box-sizing: border-box;
283 | padding: 2em 3em;
284 | }
285 |
286 | .user-view h1 {
287 | margin: 0;
288 | font-size: 1.5em;
289 | }
290 |
291 | .user-view .meta {
292 | list-style-type: none;
293 | padding: 0;
294 | }
295 |
296 | .user-view .label {
297 | display: inline-block;
298 | min-width: 4em;
299 | }
300 |
301 | .user-view .about {
302 | margin: 1em 0;
303 | }
304 |
305 | .user-view .links a {
306 | text-decoration: underline;
307 | }
308 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/src/app/data/useLoader.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | Resource,
3 | ResourceFetcher,
4 | ResourceFetcherInfo,
5 | ResourceOptions,
6 | Signal,
7 | } from 'solid-js'
8 | import {
9 | createResource,
10 | onCleanup,
11 | startTransition,
12 | untrack,
13 | useContext,
14 | } from 'solid-js'
15 | import type { ReconcileOptions } from 'solid-js/store'
16 | import { createStore, reconcile, unwrap } from 'solid-js/store'
17 | import { isServer } from 'solid-js/web'
18 | import { useNavigate } from '@solidjs/router'
19 | import { isRedirectResponse, LocationHeader } from '@tanstack/bling'
20 | // import { ServerContext } from '../server/ServerContext'
21 | // import { FETCH_EVENT, ServerFunctionEvent } from '../server/types'
22 |
23 | interface RouteDataEvent {}
24 |
25 | type RouteDataSource =
26 | | S
27 | | false
28 | | null
29 | | undefined
30 | | (() => S | false | null | undefined)
31 |
32 | type RouteDataFetcher = (
33 | source: S,
34 | event: RouteDataEvent,
35 | ) => T | Promise
36 |
37 | type RouteDataOptions = ResourceOptions & {
38 | key?: RouteDataSource
39 | reconcileOptions?: ReconcileOptions
40 | }
41 |
42 | const resources = new Set<(k: any) => void>()
43 | const promises = new Map>()
44 |
45 | export function useLoader(
46 | fetcher: RouteDataFetcher,
47 | options?: RouteDataOptions,
48 | ): Resource
49 | export function useLoader(
50 | fetcher: RouteDataFetcher,
51 | options: RouteDataOptions,
52 | ): Resource
53 | export function useLoader(
54 | fetcher?: RouteDataFetcher,
55 | options: RouteDataOptions | RouteDataOptions = {},
56 | ): Resource | Resource {
57 | const navigate = useNavigate()
58 | // const pageEvent = useContext(ServerContext)
59 |
60 | function handleResponse(response: Response) {
61 | if (isRedirectResponse(response)) {
62 | startTransition(() => {
63 | let url = response.headers.get(LocationHeader)
64 | if (url && url.startsWith('/')) {
65 | navigate(url, {
66 | replace: true,
67 | })
68 | } else {
69 | if (!isServer && url) {
70 | window.location.href = url
71 | }
72 | }
73 | })
74 | // if (isServer && pageEvent) {
75 | // pageEvent.setStatusCode(response.status)
76 | // response.headers.forEach((head, value) => {
77 | // pageEvent.responseHeaders.set(value, head)
78 | // })
79 | // }
80 | }
81 | }
82 |
83 | const resourceFetcher: ResourceFetcher = async (key: S) => {
84 | try {
85 | // let event = pageEvent as RouteDataEvent
86 | // if (isServer && pageEvent) {
87 | // event = Object.freeze({
88 | // request: pageEvent.request,
89 | // env: pageEvent.env,
90 | // clientAddress: pageEvent.clientAddress,
91 | // locals: pageEvent.locals,
92 | // $type: FETCH_EVENT,
93 | // fetch: pageEvent.fetch,
94 | // })
95 | // }
96 |
97 | let response = await (fetcher as any).call({}, key)
98 | if (response instanceof Response) {
99 | if (isServer) {
100 | handleResponse(response)
101 | } else {
102 | setTimeout(() => handleResponse(response), 0)
103 | }
104 | }
105 | return response
106 | } catch (e: any | Error) {
107 | if (e instanceof Response) {
108 | if (isServer) {
109 | handleResponse(e)
110 | } else {
111 | setTimeout(() => handleResponse(e), 0)
112 | }
113 | return e
114 | }
115 | throw e
116 | }
117 | }
118 |
119 | function dedupe(fetcher: ResourceFetcher): ResourceFetcher {
120 | return (key: S, info: ResourceFetcherInfo) => {
121 | if (
122 | info.refetching &&
123 | info.refetching !== true &&
124 | !partialMatch(key, info.refetching) &&
125 | info.value
126 | ) {
127 | return info.value
128 | }
129 |
130 | if (key == true) return fetcher(key, info)
131 |
132 | let promise = promises.get(key)
133 | if (promise) return promise
134 | promise = fetcher(key, info) as Promise
135 | promises.set(key, promise)
136 | return promise.finally(() => promises.delete(key))
137 | }
138 | }
139 |
140 | const [resource, { refetch }] = createResource(
141 | (options.key || true) as RouteDataSource,
142 | dedupe(resourceFetcher),
143 | {
144 | storage: (init: T | undefined) =>
145 | createDeepSignal(init, options.reconcileOptions),
146 | ...options,
147 | } as any,
148 | )
149 |
150 | if (!isServer) {
151 | resources.add(refetch)
152 | onCleanup(() => resources.delete(refetch))
153 | }
154 |
155 | return resource
156 | }
157 |
158 | export function refetchLoaders(key?: string | any[] | void) {
159 | if (isServer) throw new Error('Cannot refetch route data on the server.')
160 | return startTransition(() => {
161 | for (let refetch of resources) refetch(key)
162 | })
163 | }
164 |
165 | function createDeepSignal(value: T, options?: ReconcileOptions) {
166 | const [store, setStore] = createStore({
167 | value,
168 | })
169 | return [
170 | () => store.value,
171 | (v: T) => {
172 | const unwrapped = untrack(() => unwrap(store.value))
173 | typeof v === 'function' && (v = v(unwrapped))
174 | setStore('value', reconcile(v, options))
175 | return store.value
176 | },
177 | ] as Signal
178 | }
179 |
180 | /* React Query key matching https://github.com/tannerlinsley/react-query */
181 | function partialMatch(a: any, b: any) {
182 | return partialDeepEqual(ensureQueryKeyArray(a), ensureQueryKeyArray(b))
183 | }
184 |
185 | function ensureQueryKeyArray(
186 | value: V,
187 | ): R {
188 | return (Array.isArray(value) ? value : [value]) as R
189 | }
190 |
191 | /**
192 | * Checks if `b` partially matches with `a`.
193 | */
194 | function partialDeepEqual(a: any, b: any): boolean {
195 | if (a === b) {
196 | return true
197 | }
198 |
199 | if (typeof a !== typeof b) {
200 | return false
201 | }
202 |
203 | if (a.length && !b.length) return false
204 |
205 | if (a && b && typeof a === 'object' && typeof b === 'object') {
206 | return !Object.keys(b).some((key) => !partialDeepEqual(a[key], b[key]))
207 | }
208 |
209 | return false
210 | }
211 |
--------------------------------------------------------------------------------
/packages/bling/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AnyFetchFn,
3 | JsonResponse,
4 | Serializer,
5 | Fetcher,
6 | FetcherFn,
7 | FetcherMethods,
8 | FetchFnCtx,
9 | FetchFnCtxOptions,
10 | FetchFnCtxWithRequest,
11 | } from '../types'
12 |
13 | export const XBlingStatusCodeHeader = 'x-bling-status-code'
14 | export const XBlingLocationHeader = 'x-bling-location'
15 | export const LocationHeader = 'Location'
16 | export const ContentTypeHeader = 'content-type'
17 | export const XBlingResponseTypeHeader = 'x-bling-response-type'
18 | export const XBlingContentTypeHeader = 'x-bling-content-type'
19 | export const XBlingOrigin = 'x-bling-origin'
20 | export const JSONResponseType = 'application/json'
21 |
22 | /**
23 | * A JSON response. Converts `data` to JSON and sets the `Content-Type` header.
24 | */
25 | export function json(
26 | data: TData,
27 | init: number | ResponseInit = {},
28 | ): JsonResponse {
29 | let responseInit: any = init
30 | if (typeof init === 'number') {
31 | responseInit = { status: init }
32 | }
33 |
34 | let headers = new Headers(responseInit.headers)
35 |
36 | if (!headers.has(ContentTypeHeader)) {
37 | headers.set(ContentTypeHeader, 'application/json; charset=utf-8')
38 | }
39 |
40 | headers.set(XBlingContentTypeHeader, 'json')
41 |
42 | const response = new Response(JSON.stringify(data), {
43 | ...responseInit,
44 | headers,
45 | })
46 |
47 | return response
48 | }
49 |
50 | /**
51 | * A redirect response. Sets the status code and the `Location` header.
52 | * Defaults to "302 Found".
53 | */
54 | export function redirect(
55 | url: string,
56 | init: number | ResponseInit = 302,
57 | ): Response {
58 | let responseInit = init
59 | if (typeof responseInit === 'number') {
60 | responseInit = { status: responseInit }
61 | } else if (typeof responseInit.status === 'undefined') {
62 | responseInit.status = 302
63 | }
64 |
65 | if (url === '') {
66 | url = '/'
67 | }
68 |
69 | if (process.env.NODE_ENV === 'development') {
70 | if (url.startsWith('.')) {
71 | throw new Error('Relative URLs are not allowed in redirect')
72 | }
73 | }
74 |
75 | let headers = new Headers(responseInit.headers)
76 | headers.set(LocationHeader, url)
77 |
78 | const response = new Response(null, {
79 | ...responseInit,
80 | headers: headers,
81 | })
82 |
83 | return response
84 | }
85 |
86 | export function eventStream(
87 | request: Request,
88 | init: (send: (event: string, data: any) => void) => () => void,
89 | ) {
90 | let stream = new ReadableStream({
91 | start(controller) {
92 | let encoder = new TextEncoder()
93 | let send = (event: string, data: any) => {
94 | controller.enqueue(encoder.encode('event: ' + event + '\n'))
95 | controller.enqueue(encoder.encode('data: ' + data + '\n' + '\n'))
96 | }
97 | let cleanup = init(send)
98 | let closed = false
99 | let close = () => {
100 | if (closed) return
101 | cleanup()
102 | closed = true
103 | request.signal.removeEventListener('abort', close)
104 | controller.close()
105 | }
106 | request.signal.addEventListener('abort', close)
107 | if (request.signal.aborted) {
108 | close()
109 | return
110 | }
111 | },
112 | })
113 | return new Response(stream, {
114 | headers: { 'Content-Type': 'text/event-stream' },
115 | })
116 | }
117 |
118 | export function isResponse(value: any): value is Response {
119 | return (
120 | value != null &&
121 | typeof value.status === 'number' &&
122 | typeof value.statusText === 'string' &&
123 | typeof value.headers === 'object' &&
124 | typeof value.body !== 'undefined'
125 | )
126 | }
127 |
128 | const redirectStatusCodes = new Set([204, 301, 302, 303, 307, 308])
129 |
130 | export function isRedirectResponse(
131 | response: Response | any,
132 | ): response is Response {
133 | return (
134 | response &&
135 | response instanceof Response &&
136 | redirectStatusCodes.has(response.status)
137 | )
138 | }
139 |
140 | export function mergeHeaders(...objs: (Headers | HeadersInit | undefined)[]) {
141 | const allHeaders: any = {}
142 |
143 | for (const header of objs) {
144 | if (!header) continue
145 | const headers: Headers = new Headers(header)
146 |
147 | for (const [key, value] of (headers as any).entries()) {
148 | if (value === undefined || value === 'undefined') {
149 | delete allHeaders[key]
150 | } else {
151 | allHeaders[key] = value
152 | }
153 | }
154 | }
155 |
156 | return new Headers(allHeaders)
157 | }
158 |
159 | export function mergeRequestInits(...objs: (RequestInit | undefined)[]) {
160 | return Object.assign({}, ...objs, {
161 | headers: mergeHeaders(...objs.map((o) => o && o.headers)),
162 | })
163 | }
164 |
165 | export async function parseResponse(response: Response) {
166 | if (response instanceof Response) {
167 | const contentType =
168 | response.headers.get(XBlingContentTypeHeader) ||
169 | response.headers.get(ContentTypeHeader) ||
170 | ''
171 |
172 | if (contentType.includes('json')) {
173 | return await response.json()
174 | } else if (contentType.includes('text')) {
175 | return await response.text()
176 | } else if (contentType.includes('error')) {
177 | const data = await response.json()
178 | const error = new Error(data.error.message)
179 | if (data.error.stack) {
180 | error.stack = data.error.stack
181 | }
182 | return error
183 | } else if (contentType.includes('response')) {
184 | if (response.status === 204 && response.headers.get(LocationHeader)) {
185 | return redirect(response.headers.get(LocationHeader)!)
186 | }
187 | return response
188 | } else {
189 | if (response.status === 200) {
190 | const text = await response.text()
191 | try {
192 | return JSON.parse(text)
193 | } catch {}
194 | }
195 | if (response.status === 204 && response.headers.get(LocationHeader)) {
196 | return redirect(response.headers.get(LocationHeader)!)
197 | }
198 | return response
199 | }
200 | }
201 |
202 | return response
203 | }
204 |
205 | export function mergeFetchOpts(
206 | ...objs: (FetchFnCtxOptions | undefined)[]
207 | ): FetchFnCtxWithRequest {
208 | return Object.assign({}, [
209 | ...objs,
210 | {
211 | request: mergeRequestInits(...objs.map((o) => o && o.request)),
212 | },
213 | ]) as any
214 | }
215 |
216 | export function payloadRequestInit(
217 | payload: any,
218 | serializers: false | Serializer[],
219 | ) {
220 | let req: RequestInit = {}
221 |
222 | if (payload instanceof FormData) {
223 | req.body = payload
224 | } else {
225 | req.body = JSON.stringify(
226 | payload,
227 | serializers
228 | ? (key, value) => {
229 | let serializer = serializers.find(({ apply }) => apply(value))
230 | if (serializer) {
231 | return serializer.serialize(value)
232 | }
233 | return value
234 | }
235 | : undefined,
236 | )
237 |
238 | req.headers = {
239 | [ContentTypeHeader]: JSONResponseType,
240 | }
241 | }
242 |
243 | return req
244 | }
245 |
246 | export function createFetcher(
247 | route: string,
248 | fetcherImpl: FetcherFn,
249 | ): Fetcher {
250 | const fetcherMethods: FetcherMethods = {
251 | url: route,
252 | fetch: (request: RequestInit, ctx?: FetchFnCtxOptions) => {
253 | return fetcherImpl({} as any, mergeFetchOpts({ request }, ctx))
254 | },
255 | }
256 |
257 | return Object.assign(fetcherImpl, fetcherMethods) as Fetcher
258 | }
259 |
260 | export function resolveRequestHref(
261 | pathname: string,
262 | method: 'GET' | 'POST',
263 | payloadInit: RequestInit,
264 | ) {
265 | const resolved =
266 | method.toLowerCase() === 'get'
267 | ? `${pathname}?payload=${encodeURIComponent(payloadInit.body as string)}`
268 | : pathname
269 |
270 | return new URL(
271 | resolved,
272 | typeof document !== 'undefined' ? window.location.href : `http://localhost`,
273 | ).href
274 | }
275 |
--------------------------------------------------------------------------------
/examples/astro-solid-hackernews/src/app/root.tsx:
--------------------------------------------------------------------------------
1 | import { server$, import$, secret$, redirect } from '@tanstack/bling'
2 | import {
3 | createSignal,
4 | ErrorBoundary,
5 | lazy,
6 | Show,
7 | Suspense,
8 | useContext,
9 | } from 'solid-js'
10 | import { HydrationScript, NoHydration } from 'solid-js/web'
11 | import { manifestContext } from './manifest'
12 | import {
13 | A,
14 | Link,
15 | Outlet,
16 | RouteDataFuncArgs,
17 | RouteDefinition,
18 | useRouteData,
19 | } from '@solidjs/router'
20 | import Nav from './components/nav'
21 | import './root.css'
22 | import { Component, createResource, For } from 'solid-js'
23 | import type { IStory } from './types'
24 | import fetchAPI from './lib/api'
25 | import Story from './components/story'
26 | import Comment from './components/comment'
27 | const sayHello = server$(() => console.log('Hello world'))
28 |
29 | const LazyHello3 = lazy(() =>
30 | import$({
31 | default: () => {
32 | return (
33 | <>
34 |
35 | >
36 | )
37 | },
38 | }),
39 | )
40 |
41 | const inlineSecret = secret$('I am an inline server secret!')
42 |
43 | function App() {
44 | console.log(
45 | 'Do you know the inline server secret?',
46 | inlineSecret ?? 'Not even.',
47 | )
48 |
49 | return (
50 |
51 |
52 | SolidStart - Hacker News
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | Loading...}>
61 |
62 |
63 |
64 |
65 |
66 |
67 | )
68 | }
69 |
70 | function Scripts() {
71 | const manifest = useContext(manifestContext)
72 | return (
73 |
74 |
75 | {import.meta.env.DEV ? (
76 | <>
77 |
78 |
83 | >
84 | ) : (
85 | <>
86 |
91 | >
92 | )}
93 |
94 | )
95 | }
96 |
97 | const mapStories = {
98 | top: 'news',
99 | new: 'newest',
100 | show: 'show',
101 | ask: 'ask',
102 | job: 'jobs',
103 | } as const
104 |
105 | const storiesRouteData = ({ location, params }: RouteDataFuncArgs) => {
106 | const page = () => +location.query.page || 1
107 | const type = () => (params.stories || 'top') as keyof typeof mapStories
108 |
109 | const [stories] = createResource(
110 | () => `${mapStories[type()]}?page=${page()}`,
111 | fetchAPI,
112 | )
113 |
114 | return { type, stories, page }
115 | }
116 |
117 | const StoriesRoute: Component = () => {
118 | const { page, type, stories } = useRouteData()
119 | return (
120 |
121 |
122 |
1}
124 | fallback={
125 |
126 | {'<'} prev
127 |
128 | }
129 | >
130 |
135 | {'<'} prev
136 |
137 |
138 |
page {page()}
139 |
= 29}
141 | fallback={
142 |
143 | more {'>'}
144 |
145 | }
146 | >
147 |
152 | more {'>'}
153 |
154 |
155 |
156 |
157 |
158 |
159 | {(story) => }
160 |
161 |
162 |
163 |
164 | )
165 | }
166 |
167 | const storyRouteData = (props: RouteDataFuncArgs) => {
168 | const [story] = createResource(
169 | () => `item/${props.params.id}`,
170 | fetchAPI,
171 | )
172 | return story
173 | }
174 |
175 | const StoryRoute: Component = () => {
176 | const story = useRouteData()
177 | return (
178 |
179 |
180 |
193 |
205 |
206 |
207 | )
208 | }
209 |
210 | interface IUser {
211 | error: string
212 | id: string
213 | created: string
214 | karma: number
215 | about: string
216 | }
217 |
218 | const userRouteData = (props: RouteDataFuncArgs) => {
219 | const [user] = createResource(
220 | () => `user/${props.params.id}`,
221 | fetchAPI,
222 | )
223 | return user
224 | }
225 |
226 | const UserRoute: Component = () => {
227 | const user = useRouteData()
228 | return (
229 |
256 | )
257 | }
258 |
259 | export const routes = [
260 | {
261 | path: '/',
262 | component: App,
263 | children: [
264 | {
265 | path: '*stories',
266 | data: storiesRouteData,
267 | component: lazy(() => import$({ default: () => })),
268 | },
269 | {
270 | path: 'stories/:id',
271 | data: storyRouteData,
272 | component: lazy(() => import$({ default: () => })),
273 | },
274 | {
275 | path: 'users/:id',
276 | data: userRouteData,
277 | component: lazy(() => import$({ default: () => })),
278 | },
279 | ],
280 | },
281 | ] satisfies RouteDefinition[]
282 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/src/app/data/useAction.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate, useSearchParams, type Navigator } from '@solidjs/router'
2 | import { $TRACK, batch, createSignal, useContext } from 'solid-js'
3 | import { FormError, FormImpl, FormProps } from './Form'
4 |
5 | import type { ParentComponent } from 'solid-js'
6 | import { isRedirectResponse } from '@tanstack/bling'
7 | import { refetchLoaders } from './useLoader'
8 |
9 | interface ActionEvent {}
10 | export interface Submission {
11 | input: T
12 | result?: U
13 | error?: any
14 | clear: () => void
15 | retry: () => void
16 | }
17 |
18 | export type RouteAction = [
19 | {
20 | pending: boolean
21 | input?: T
22 | result?: U
23 | error?: any
24 | clear: () => void
25 | retry: () => void
26 | },
27 | ((vars: T) => Promise) & {
28 | Form: T extends FormData ? ParentComponent : never
29 | url: string
30 | },
31 | ]
32 | export type RouteMultiAction = [
33 | Submission[] & { pending: Submission[] },
34 | ((vars: T) => Promise) & {
35 | Form: T extends FormData ? ParentComponent : never
36 | url: string
37 | },
38 | ]
39 |
40 | export type Invalidate =
41 | | ((r: Response) => string | any[] | void)
42 | | string
43 | | any[]
44 |
45 | export function useAction(
46 | fn: (args: T) => Promise,
47 | options?: { invalidate?: Invalidate },
48 | ): RouteAction
49 | export function useAction(
50 | fn: (args: T) => Promise,
51 | options: { invalidate?: Invalidate } = {},
52 | ): RouteAction {
53 | let init: { result?: { data?: U; error?: any }; input?: T } =
54 | checkFlash(fn)
55 | const [input, setInput] = createSignal(init.input)
56 | const [result, setResult] = createSignal<
57 | { data?: U; error?: any } | undefined
58 | >(init.result)
59 | const navigate = useNavigate()
60 | // const event = useRequest();
61 | let count = 0
62 | function submit(variables: T) {
63 | const p = fn(variables)
64 | const reqId = ++count
65 | batch(() => {
66 | setResult(undefined)
67 | setInput(() => variables)
68 | })
69 | return p
70 | .then(async (data) => {
71 | if (reqId === count) {
72 | if (data instanceof Response) {
73 | await handleResponse(data, navigate, options)
74 | } else await handleRefetch(data as any[], options)
75 | if (!data || isRedirectResponse(data)) setInput(undefined)
76 | else setResult({ data })
77 | }
78 | return data
79 | })
80 | .catch(async (e) => {
81 | if (reqId === count) {
82 | if (e instanceof Response) {
83 | await handleResponse(e, navigate, options)
84 | }
85 | if (!isRedirectResponse(e)) {
86 | setResult({ error: e })
87 | } else setInput(undefined)
88 | }
89 | return undefined
90 | }) as Promise
91 | }
92 | submit.url = (fn as any).url
93 | submit.Form = ((props: Omit) => {
94 | let url = (fn as any).url
95 | return (
96 | {
100 | submit(submission.formData as any)
101 | }}
102 | >
103 | {props.children}
104 |
105 | )
106 | }) as T extends FormData ? ParentComponent : never
107 |
108 | return [
109 | {
110 | get pending() {
111 | return !!input() && !result()
112 | },
113 | get input() {
114 | return input()
115 | },
116 | get result() {
117 | return result()?.data
118 | },
119 | get error(): any {
120 | return result()?.error
121 | },
122 | clear() {
123 | batch(() => {
124 | setInput(undefined)
125 | setResult(undefined)
126 | })
127 | },
128 | retry() {
129 | const variables = input()
130 | if (!variables) throw new Error('No submission to retry')
131 | submit(variables)
132 | },
133 | },
134 | submit,
135 | ]
136 | }
137 |
138 | export function useMultiAction(
139 | fn: (arg1: void, event: ActionEvent) => Promise,
140 | options?: { invalidate?: Invalidate },
141 | ): RouteMultiAction
142 | export function useMultiAction(
143 | fn: (args: T, event: ActionEvent) => Promise,
144 | options?: { invalidate?: Invalidate },
145 | ): RouteMultiAction
146 | export function useMultiAction(
147 | fn: (args: T, event: ActionEvent) => Promise,
148 | options: { invalidate?: Invalidate } = {},
149 | ): RouteMultiAction {
150 | let init: { result?: { data?: U; error?: any }; input?: T } =
151 | checkFlash(fn)
152 | const [submissions, setSubmissions] = createSignal[]>(
153 | init.input ? [createSubmission(init.input)[0]] : [],
154 | )
155 | const navigate = useNavigate()
156 | // const event = useContext(ServerContext)
157 |
158 | function createSubmission(variables: T) {
159 | let submission: {
160 | input: T
161 | readonly result: U | undefined
162 | readonly error: Error | undefined
163 | clear(): void
164 | retry(): void
165 | }
166 | const [result, setResult] = createSignal<{ data?: U; error?: any }>()
167 | return [
168 | (submission = {
169 | input: variables,
170 | get result() {
171 | return result()?.data
172 | },
173 | get error() {
174 | return result()?.error
175 | },
176 | clear() {
177 | setSubmissions((v) => v.filter((i) => i.input !== variables))
178 | },
179 | retry() {
180 | setResult(undefined)
181 | return event && handleSubmit(fn(variables, event))
182 | },
183 | }),
184 | handleSubmit,
185 | ] as const
186 | function handleSubmit(
187 | p: Promise<(Response & { body: U }) | U>,
188 | ): Promise {
189 | p.then(async (data) => {
190 | if (data instanceof Response) {
191 | await handleResponse(data, navigate, options)
192 | data = data.body
193 | } else await handleRefetch(data as any[], options)
194 | data ? setResult({ data }) : submission.clear()
195 |
196 | return data
197 | }).catch(async (e) => {
198 | if (e instanceof Response) {
199 | await handleResponse(e, navigate, options)
200 | } else await handleRefetch(e, options)
201 | if (!isRedirectResponse(e)) {
202 | setResult({ error: e })
203 | } else submission.clear()
204 | })
205 | return p as Promise
206 | }
207 | }
208 | function submit(variables: T) {
209 | if (!event) {
210 | throw new Error('submit was called without an event')
211 | }
212 | const [submission, handleSubmit] = createSubmission(variables)
213 | setSubmissions((s) => [...s, submission])
214 | return handleSubmit(fn(variables, event))
215 | }
216 | submit.url = (fn as any).url
217 | submit.Form = ((props: FormProps) => {
218 | let url = (fn as any).url
219 | return (
220 | {
224 | submit(submission.formData as any)
225 | }}
226 | >
227 | {props.children}
228 |
229 | )
230 | }) as T extends FormData ? ParentComponent : never
231 |
232 | return [
233 | new Proxy[] & { pending: Submission[] }>([] as any, {
234 | get(_, property) {
235 | if (property === $TRACK) return submissions()
236 | if (property === 'pending')
237 | return submissions().filter((sub) => !sub.result)
238 | return submissions()[property as keyof typeof submissions]
239 | },
240 | }),
241 | submit,
242 | ]
243 | }
244 |
245 | function handleRefetch(
246 | response: Response | string | any[],
247 | options: { invalidate?: Invalidate } = {},
248 | ) {
249 | return refetchLoaders(
250 | typeof options.invalidate === 'function'
251 | ? options.invalidate(response as Response)
252 | : options.invalidate,
253 | )
254 | }
255 |
256 | function handleResponse(
257 | response: Response,
258 | navigate: Navigator,
259 | options?: { invalidate?: Invalidate },
260 | ) {
261 | if (response instanceof Response && isRedirectResponse(response)) {
262 | const locationUrl = response.headers.get('Location') || '/'
263 | if (locationUrl.startsWith('http')) {
264 | window.location.href = locationUrl
265 | } else {
266 | navigate(locationUrl)
267 | }
268 | }
269 |
270 | return handleRefetch(response, options)
271 | }
272 |
273 | function checkFlash(fn: any) {
274 | const [params] = useSearchParams()
275 |
276 | let param = params.form ? JSON.parse(params.form) : null
277 | if (!param || param.url !== (fn as any).url) {
278 | return {}
279 | }
280 |
281 | const input = new Map(param.entries)
282 | return {
283 | result: {
284 | error: param.error
285 | ? new FormError(param.error.message, {
286 | fieldErrors: param.error.fieldErrors,
287 | stack: param.error.stack,
288 | form: param.error.form,
289 | fields: param.error.fields,
290 | })
291 | : undefined,
292 | },
293 | input: input as unknown as T,
294 | }
295 | }
296 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @TanStack/Bling
2 |
3 | Framework agnostic transpilation utilities for client/server RPCs, env isolation, islands, module splitting, and more.
4 |
5 | =======
6 |
7 | # API
8 |
9 | ## Macros
10 |
11 |
12 | server$
13 |
14 | The `server$` function is used to create an isomorphic server-side RPC. It takes a function and an optional configuration object and returns a function that can be called on both server (ssr or ssg) and client. The function passed to `server$` will only be executed on the server. On the client, a `fetch` call is made to the server function instead. The results of the function will be exactly the same on both server and client.
15 |
16 | **🧠 Important Notes**:
17 |
18 | - The server-side function must be an `async` function.
19 | - The fetch calls made by the client default to using the `POST` method and passing arguments via the request body. To use `GET` requests and search-param payloads instead, the `opts.method` can be set to `GET`. This will automatically configure both the method and the payload serialization to work via search params instead of a request body. You can also alter the actual request (and request body) manually to your liking.
20 |
21 | ```tsx
22 | import { server$ } from '@tanstack/bling'
23 |
24 | const fetchFn = server$(async (payload) => {
25 | // do something
26 | return 'result'
27 | })
28 | ```
29 |
30 | ### Signature
31 |
32 | ```tsx
33 | server$ Promise>(fn: T, options: {
34 | method?: 'POST' | 'GET' // Defaults to `POST`
35 | request?: RequestInit
36 | }): T
37 | ```
38 |
39 | ### Arguments
40 |
41 | - `fn`
42 | - The function to be called from the client-side.
43 | - Arguments
44 | - `payload`
45 | - The payload passed from the client-side.
46 | - `ctx`
47 | - The context object passed from the client-side.
48 | - `request`
49 | - The request object passed from the client-side.
50 | - Returns the data or response to be sent back to the client-side
51 | - `Promise`
52 | - Can use utilities like `json`, `redirect`, or `eventStream` to return convenient responses.
53 | - `options`
54 | - `method`
55 | - The HTTP method to use when calling the server-side function.
56 | - Defaults to `POST`
57 | - If `GET` is used, the payload will automatically be encoded as query parameters.
58 | - `request`
59 | - The default request object to be passed to the `fetch` call to the server function.
60 | - Can be used to add headers, signals, etc.
61 |
62 | ### Returns
63 |
64 | A function that can be called isomorphically from server or client side code to execute the server-side function.
65 |
66 | - ```tsx
67 | fn(
68 | payload: JSON,
69 | options: {
70 | method?: 'POST' | 'GET' // Defaults to `POST`
71 | request?: RequestInit
72 | }
73 | ) => Promise<
74 | Awaited> extends JsonResponse
75 | ? R
76 | : ReturnType
77 | >
78 | ```
79 |
80 | - Arguments
81 | - `payload`
82 | - The payload to be passed to the server-side function.
83 | - `options`
84 | - `method`
85 | - The HTTP method to use when calling the server-side function.
86 | - Defaults to `POST`
87 | - If `GET` is used, the payload will automatically be encoded as query parameters.
88 | - `request`
89 | - The request object to be passed to the `fetch` call to the server function.
90 | - Can be used to add headers, signals, etc.
91 | - Returns
92 | - If a plain Response is returned in the server function, it will be returned here.
93 | - If a redirect is returned or thrown in the server function, the redirect will be followed.
94 | - All other values will be treated as JSON. For type-safe JSON, use the `json(data, responseInit)` utility
95 |
96 | - `fn.fetch`
97 |
98 | - A convenience `fn.fetch` method is also exposed on the function itself to facilitate custom fetch calls. In this case, only the request object is passed as the first argument. Any data you wish to pass should be encoded in the request object.
99 |
100 | ```tsx
101 | fn.fetch(
102 | request: RequestInit,
103 | ) => Promise<
104 | Awaited> extends JsonResponse
105 | ? R
106 | : ReturnType
107 | >
108 | ```
109 |
110 | - Arguments
111 | - `payload`
112 | - The payload to be passed to the server-side function.
113 | - `options`
114 | - `request`
115 | - The request object to be passed to the `fetch` call to the server function.
116 | - Can be used to add headers, signals, etc.
117 |
118 |
119 |
120 |
121 | secret$
122 |
123 | ## `secret$`
124 |
125 | The `secret$` function can be used to scope any expression to the server (secret)-bundle only. This means that the expression will be removed from the client bundle. This is useful for things like server-side only imports, server-side only code or sensitive env variables that should never be available on the client.
126 |
127 | ```tsx
128 | import { secret$ } from '@tanstack/bling'
129 |
130 | const secretMessage = secret$('It is a secret!')')
131 | ```
132 |
133 | Server Output:
134 |
135 | ```tsx
136 | const secretMessage = server$('It is a secret!')')
137 | ```
138 |
139 | Client Output:
140 |
141 | ```tsx
142 | const secretMessage = undefined
143 | ```
144 |
145 | ### Signature
146 |
147 | ```tsx
148 | secret$(input: T): T
149 | ```
150 |
151 | > 🧠 The return type is the same as the input type. Although the value could technically be `undefined` on the client, it's more useful to retain a non-nullable type in the wild.
152 |
153 | ### Arguments
154 |
155 | - `input`
156 | - Any function, expression, or variable.
157 |
158 | ### Returns
159 |
160 | - The variable on the server
161 | - `undefined` on the client
162 |
163 |
164 |
165 |
166 | import$
167 |
168 | ## `import$`
169 |
170 | The `import$` function can be used to code-split any expression into it's own module on both server and client at build-time. This is helpful for you to coordinate what code loads when without having to create new files for every part you want want to code-split. It's an async function just like the native dynamic import. It actually compiles down to a dynamic import, but with a unique hash for each import$ instance used in the file.
171 |
172 | ```tsx
173 | import { import$ } from '@tanstack/bling'
174 |
175 | const fn = await import$(async (name: string) => {
176 | return `Hello ${name}`
177 | })
178 | ```
179 |
180 | This can be used to code-split React/Solid components too:
181 |
182 | ```tsx
183 | import { import$ } from '@tanstack/bling'
184 | import { lazy } from 'react'
185 |
186 | const fn = lazy(() => import$({
187 | default: () => Hello World!
,
188 | }))
189 | ```
190 |
191 | Output:
192 |
193 | ```tsx
194 | const fn = await import('/this/file?split=0&ref=fn').then((m) => m.default)
195 | ```
196 |
197 | ### Signature
198 |
199 | ```tsx
200 | import$(fn: T) => Promise
201 | ```
202 |
203 | ### Arguments
204 |
205 | - `value`
206 | - The value/expression/function to be code-split.
207 |
208 | ### Returns
209 |
210 | - A code-split version of the original expression.
211 |
212 |
213 |
214 | ## File conventions
215 |
216 |
217 | Secret files
218 |
219 | ## Server-Only Files
220 |
221 | The `[filename].secret.[ext]` pattern can be used to create server-side only files. These files will be removed from the client bundle. This is useful for things like server-side only imports, or server-side only code. It works with any file name and extension so long as `.server$.` is found in the resolved file pathname.
222 |
223 | When a server-only file is imported on the client, it will be provided the same exports, but stubbed with undefined values. Don't put anything sensitive in the exported variable name! 😜
224 |
225 | ```tsx
226 | // secret.server$.ts`
227 | export const secret = 'This is top secret!'
228 | export const anotherSecret = '🤫 Shhh!'
229 | ```
230 |
231 | Client output:
232 |
233 | ```tsx
234 | export const secret = undefined
235 | export const anotherSecret = undefined
236 | ```
237 |
238 |
239 |
240 | ## Proposed APIs
241 |
242 | The following APIs are proposed for future versions of Bling. They are not yet implemented, but are being considered for future releases.
243 |
244 |
245 | worker$
246 |
247 | ## `worker$`
248 |
249 | The `worker$` function is used to create an isomorphic Web Worker and interact with it. On the server, the function will run in the same process as the server. On the client, the function will be compiled to a Web Worker and will return an interface similar to `server$` to make it easy to call from the client
250 |
251 | > 🧠 Similar to `server$`, data sent to and from workers will be serialized. This means that you can pass any JSON-serializable data to the worker, but you cannot pass functions or classes. If you need to use non-serializable assets in your worker, you can import them and use them directly in the worker function, however the instances of those assets will be unique to the worker thread.
252 |
253 | ```tsx
254 | import { worker$ } from '@tanstack/bling'
255 |
256 | const sayHello = worker$(async (name: string) => {
257 | // do something
258 | return `Hello ${name}`
259 | })
260 |
261 | const result = sayHello('World!')
262 | console.log(result) // 'Hello World!'
263 | ```
264 |
265 |
266 |
267 |
268 | - [`websocket$`](#websocket)
269 | - [`lazy$`](#lazy)
270 | - [`interactive$`/`island$`](#interactive)
271 |
--------------------------------------------------------------------------------
/packages/bling/src/server.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ContentTypeHeader,
3 | JSONResponseType,
4 | LocationHeader,
5 | XBlingContentTypeHeader,
6 | XBlingLocationHeader,
7 | XBlingOrigin,
8 | XBlingResponseTypeHeader,
9 | createFetcher,
10 | isRedirectResponse,
11 | mergeRequestInits,
12 | parseResponse,
13 | payloadRequestInit,
14 | resolveRequestHref,
15 | } from './utils/utils'
16 | import type {
17 | AnyFetchFn,
18 | Deserializer,
19 | Fetcher,
20 | FetchFnCtx,
21 | CreateFetcherFn,
22 | FetchFnCtxOptions,
23 | FetchFnCtxWithRequest,
24 | SplitFn,
25 | CreateSplitFn,
26 | CreateSecretFn,
27 | CreateImportFn,
28 | CreateLazyFn,
29 | } from './types'
30 |
31 | export * from './utils/utils'
32 |
33 | const deserializers: Deserializer[] = []
34 |
35 | export function addDeserializer(deserializer: Deserializer) {
36 | deserializers.push(deserializer)
37 | }
38 |
39 | export type ServerFetcherMethods = {
40 | createHandler(
41 | fn: AnyFetchFn,
42 | pathame: string,
43 | opts: FetchFnCtxOptions,
44 | ): Fetcher
45 | registerHandler(pathname: string, handler: Fetcher): void
46 | }
47 |
48 | export type FetchFn = CreateFetcherFn & ServerFetcherMethods
49 |
50 | const serverImpl = (() => {
51 | throw new Error('Should be compiled away')
52 | }) as any
53 |
54 | const serverMethods: ServerFetcherMethods = {
55 | createHandler: (
56 | fn: AnyFetchFn,
57 | pathname: string,
58 | defaultOpts?: FetchFnCtxOptions,
59 | ): Fetcher => {
60 | return createFetcher(pathname, async (payload: any, opts?: FetchFnCtx) => {
61 | const method = opts?.method || defaultOpts?.method || 'POST'
62 |
63 | console.log(`Executing server function: ${method} ${pathname}`)
64 | if (payload) {
65 | console.log(` Fn Payload:`)
66 | console.log(payload)
67 | }
68 |
69 | opts = opts ?? {}
70 |
71 | if (!opts.__hasRequest) {
72 | // This will happen if the server function is called directly during SSR
73 | // Even though we're not crossing the network, we still need to
74 | // create a Request object to pass to the server function as if it was
75 |
76 | let payloadInit = payloadRequestInit(payload, false)
77 |
78 | const resolvedHref = resolveRequestHref(pathname, method, payloadInit)
79 | opts.request = new Request(
80 | resolvedHref,
81 | mergeRequestInits(
82 | {
83 | method: 'POST',
84 | headers: {
85 | [XBlingOrigin]: 'server',
86 | },
87 | },
88 | payloadInit,
89 | defaultOpts?.request,
90 | opts?.request,
91 | ),
92 | )
93 | }
94 |
95 | try {
96 | // Do the same parsing of the result as we do on the client
97 | const response = await fn(payload, opts as FetchFnCtxWithRequest)
98 |
99 | if (!opts.__hasRequest) {
100 | // If we're on the server during SSR, we can skip to
101 | // parsing the response directly
102 | return parseResponse(response)
103 | }
104 |
105 | // Otherwise, the client-side code will parse the response properly
106 | return response
107 | } catch (e) {
108 | if (e instanceof Error && /[A-Za-z]+ is not defined/.test(e.message)) {
109 | const error = new Error(
110 | e.message +
111 | '\n' +
112 | ' You probably are using a variable defined in a closure in your server function. Make sure you pass any variables needed to the server function as arguments. These arguments must be serializable.',
113 | )
114 | error.stack = e.stack ?? ''
115 | throw error
116 | }
117 | throw e
118 | }
119 | })
120 | },
121 | registerHandler(pathname: string, handler: Fetcher): any {
122 | console.log('Registering handler', pathname)
123 | handlers.set(pathname, handler)
124 | },
125 | }
126 |
127 | export const fetch$: FetchFn = Object.assign(serverImpl, serverMethods)
128 |
129 | export const server$ = fetch$
130 |
131 | export async function handleFetch$(
132 | _ctx: Omit,
133 | ) {
134 | if (!_ctx.request) {
135 | throw new Error('handleEvent must be called with a request.')
136 | }
137 |
138 | const ctx: FetchFnCtxWithRequest = { ..._ctx, __hasRequest: true }
139 |
140 | const url = new URL(ctx.request.url)
141 |
142 | if (hasHandler(url.pathname)) {
143 | try {
144 | let [pathname, payload] = await parseRequest(ctx)
145 | let handler = getHandler(pathname)
146 | if (!handler) {
147 | throw {
148 | status: 404,
149 | message: 'Handler Not Found for ' + pathname,
150 | }
151 | }
152 | const data = await handler(payload, { ...ctx, __hasRequest: true })
153 | return respondWith(ctx, data, 'return')
154 | } catch (error) {
155 | return respondWith(ctx, error as Error, 'throw')
156 | }
157 | }
158 |
159 | return null
160 | }
161 |
162 | async function parseRequest(event: FetchFnCtxWithRequest) {
163 | let request = event.request
164 | let contentType = request.headers.get(ContentTypeHeader)
165 | let pathname = new URL(request.url).pathname,
166 | payload
167 |
168 | // Get request have their payload in the query string
169 | if (request.method.toLowerCase() === 'get') {
170 | let url = new URL(request.url)
171 | let params = new URLSearchParams(url.search)
172 | const payloadSearchStr = params.get('payload')
173 | if (payloadSearchStr) {
174 | let payloadStr = decodeURIComponent(params.get('payload') ?? '')
175 | payload = JSON.parse(payloadStr)
176 | }
177 | } else if (contentType) {
178 | // Post requests have their payload in the body
179 | if (contentType === JSONResponseType) {
180 | let text = await request.text()
181 | try {
182 | payload = JSON.parse(text, (key: string, value: any) => {
183 | if (!value) {
184 | return value
185 | }
186 |
187 | let deserializer = deserializers.find((d) => d.apply(value))
188 | if (deserializer) {
189 | return deserializer.deserialize(value, event)
190 | }
191 | return value
192 | })
193 | } catch (e) {
194 | throw new Error(`Error parsing request body: ${text}\n ${e}`)
195 | }
196 | } else if (contentType.includes('form')) {
197 | let formData = await request.clone().formData()
198 | payload = formData
199 | }
200 | }
201 |
202 | return [pathname, payload]
203 | }
204 |
205 | function respondWith(
206 | ctx: FetchFnCtxWithRequest,
207 | data: Response | Error | string | object,
208 | responseType: 'throw' | 'return',
209 | ) {
210 | if (data instanceof Response) {
211 | if (
212 | isRedirectResponse(data) &&
213 | ctx.request.headers.get(XBlingOrigin) === 'client'
214 | ) {
215 | let headers = new Headers(data.headers)
216 | headers.set(XBlingOrigin, 'server')
217 | headers.set(XBlingLocationHeader, data.headers.get(LocationHeader)!)
218 | headers.set(XBlingResponseTypeHeader, responseType)
219 | headers.set(XBlingContentTypeHeader, 'response')
220 | return new Response(null, {
221 | status: 204,
222 | statusText: 'Redirected',
223 | headers: headers,
224 | })
225 | }
226 |
227 | if (data.status === 101) {
228 | // this is a websocket upgrade, so we don't want to modify the response
229 | return data
230 | }
231 |
232 | let headers = new Headers(data.headers)
233 | headers.set(XBlingOrigin, 'server')
234 | headers.set(XBlingResponseTypeHeader, responseType)
235 | if (!headers.has(XBlingContentTypeHeader)) {
236 | headers.set(XBlingContentTypeHeader, 'response')
237 | }
238 |
239 | return new Response(data.body, {
240 | status: data.status,
241 | statusText: data.statusText,
242 | headers,
243 | })
244 | }
245 |
246 | if (data instanceof Error) {
247 | console.error(data)
248 | return new Response(
249 | JSON.stringify({
250 | error: {
251 | stack: `This error happened inside a server function and you didn't handle it. So the client will receive an Internal Server Error. You can catch the error and throw a ServerError that makes sense for your UI. In production, the user will have no idea what the error is: \n\n${data.stack}`,
252 | status: (data as any).status,
253 | },
254 | }),
255 | {
256 | status: (data as any).status || 500,
257 | headers: {
258 | [XBlingResponseTypeHeader]: responseType,
259 | [XBlingContentTypeHeader]: 'error',
260 | },
261 | },
262 | )
263 | }
264 |
265 | if (
266 | typeof data === 'object' ||
267 | typeof data === 'string' ||
268 | typeof data === 'number' ||
269 | typeof data === 'boolean'
270 | ) {
271 | return new Response(JSON.stringify(data), {
272 | status: 200,
273 | headers: {
274 | [ContentTypeHeader]: 'application/json',
275 | [XBlingResponseTypeHeader]: responseType,
276 | [XBlingContentTypeHeader]: 'json',
277 | },
278 | })
279 | }
280 |
281 | return new Response('null', {
282 | status: 200,
283 | headers: {
284 | [ContentTypeHeader]: 'application/json',
285 | [XBlingContentTypeHeader]: 'json',
286 | [XBlingResponseTypeHeader]: responseType,
287 | },
288 | })
289 | }
290 |
291 | const handlers = new Map>()
292 |
293 | export function getHandler(pathname: string) {
294 | return handlers.get(pathname)
295 | }
296 |
297 | export function hasHandler(pathname: string) {
298 | return handlers.has(pathname)
299 | }
300 |
301 | export const split$: CreateSplitFn = (_fn) => {
302 | throw new Error('Should be compiled away')
303 | }
304 |
305 | export const lazy$: CreateLazyFn = (_fn) => {
306 | throw new Error('Should be compiled away')
307 | }
308 |
309 | export const secret$: CreateSecretFn = (_value) => {
310 | throw new Error('Should be compiled away')
311 | }
312 |
313 | export const import$: CreateImportFn = (_fn) => {
314 | throw new Error('Should be compiled away')
315 | }
316 |
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/src/app.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | button {
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | background: none;
12 | font-size: 100%;
13 | vertical-align: baseline;
14 | font-family: inherit;
15 | font-weight: inherit;
16 | color: inherit;
17 | -webkit-appearance: none;
18 | appearance: none;
19 | -webkit-font-smoothing: antialiased;
20 | -moz-osx-font-smoothing: grayscale;
21 | }
22 |
23 | body {
24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
25 | line-height: 1.4em;
26 | background: #f5f5f5;
27 | color: #4d4d4d;
28 | min-width: 230px;
29 | max-width: 550px;
30 | margin: 0 auto;
31 | -webkit-font-smoothing: antialiased;
32 | -moz-osx-font-smoothing: grayscale;
33 | font-weight: 300;
34 | }
35 |
36 | :focus {
37 | outline: 0;
38 | }
39 |
40 | .hidden {
41 | display: none;
42 | }
43 |
44 | .todoapp {
45 | background: #fff;
46 | margin: 130px 0 40px 0;
47 | position: relative;
48 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
49 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
50 | }
51 |
52 | .todoapp input::-webkit-input-placeholder {
53 | font-style: italic;
54 | font-weight: 300;
55 | color: #e6e6e6;
56 | }
57 |
58 | .todoapp input::-moz-placeholder {
59 | font-style: italic;
60 | font-weight: 300;
61 | color: #e6e6e6;
62 | }
63 |
64 | .todoapp input::input-placeholder {
65 | font-style: italic;
66 | font-weight: 300;
67 | color: #e6e6e6;
68 | }
69 |
70 | .todoapp h1 {
71 | position: absolute;
72 | top: -155px;
73 | width: 100%;
74 | font-size: 100px;
75 | font-weight: 100;
76 | text-align: center;
77 | color: rgba(175, 47, 47, 0.15);
78 | -webkit-text-rendering: optimizeLegibility;
79 | -moz-text-rendering: optimizeLegibility;
80 | text-rendering: optimizeLegibility;
81 | }
82 |
83 | .new-todo,
84 | .edit {
85 | position: relative;
86 | margin: 0;
87 | width: 100%;
88 | font-size: 24px;
89 | font-family: inherit;
90 | font-weight: inherit;
91 | line-height: 1.4em;
92 | border: 0;
93 | color: inherit;
94 | padding: 6px;
95 | border: 1px solid #999;
96 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
97 | box-sizing: border-box;
98 | -webkit-font-smoothing: antialiased;
99 | -moz-osx-font-smoothing: grayscale;
100 | }
101 |
102 | .new-todo {
103 | padding: 16px 16px 16px 60px;
104 | border: none;
105 | background: rgba(0, 0, 0, 0.003);
106 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
107 | }
108 |
109 | .main {
110 | position: relative;
111 | z-index: 2;
112 | border-top: 1px solid #e6e6e6;
113 | }
114 |
115 | .toggle-all {
116 | width: 1px;
117 | height: 1px;
118 | border: none; /* Mobile Safari */
119 | opacity: 0;
120 | position: absolute;
121 | right: 100%;
122 | bottom: 100%;
123 | }
124 |
125 | .toggle-all + label {
126 | width: 60px;
127 | height: 34px;
128 | font-size: 0;
129 | position: absolute;
130 | top: -52px;
131 | left: -13px;
132 | -webkit-transform: rotate(90deg);
133 | transform: rotate(90deg);
134 | }
135 |
136 | .toggle-all + label:before {
137 | content: '❯';
138 | font-size: 22px;
139 | color: #e6e6e6;
140 | padding: 10px 27px 10px 27px;
141 | }
142 |
143 | .toggle-all:checked + label:before {
144 | color: #737373;
145 | }
146 |
147 | .todo-list {
148 | margin: 0;
149 | padding: 0;
150 | list-style: none;
151 | }
152 |
153 | .todo-list li {
154 | position: relative;
155 | font-size: 24px;
156 | border-bottom: 1px solid #ededed;
157 | }
158 |
159 | .todo-list li:last-child {
160 | border-bottom: none;
161 | }
162 |
163 | .todo-list li.editing {
164 | border-bottom: none;
165 | padding: 0;
166 | }
167 |
168 | .todo-list li.editing .edit {
169 | display: block;
170 | width: 506px;
171 | padding: 12px 16px;
172 | margin: 0 0 0 43px;
173 | }
174 |
175 | .todo-list li.editing .view {
176 | display: none;
177 | }
178 |
179 | .todo-list li .toggle {
180 | text-align: center;
181 | width: 40px;
182 | /* auto, since non-WebKit browsers doesn't support input styling */
183 | height: auto;
184 | position: absolute;
185 | top: 0;
186 | bottom: 0;
187 | margin: auto 0;
188 | border: none; /* Mobile Safari */
189 | -webkit-appearance: none;
190 | appearance: none;
191 | }
192 |
193 | .todo-list li .toggle {
194 | opacity: 0;
195 | }
196 |
197 | .todo-list li .toggle + label {
198 | /*
199 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
200 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
201 | */
202 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
203 | background-repeat: no-repeat;
204 | background-position: center left;
205 | }
206 |
207 | .todo-list li .toggle:checked + label {
208 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
209 | }
210 |
211 | .todo-list li label {
212 | word-break: break-all;
213 | padding: 15px 15px 15px 60px;
214 | display: block;
215 | line-height: 1.2;
216 | transition: color 0.4s;
217 | }
218 |
219 | .todo-list li.completed label {
220 | color: #d9d9d9;
221 | text-decoration: line-through;
222 | }
223 |
224 | .todo-list li .destroy {
225 | display: none;
226 | position: absolute;
227 | top: 0;
228 | right: 10px;
229 | bottom: 0;
230 | width: 40px;
231 | height: 40px;
232 | margin: auto 0;
233 | font-size: 30px;
234 | color: #cc9a9a;
235 | margin-bottom: 11px;
236 | transition: color 0.2s ease-out;
237 | }
238 |
239 | .todo-list li .destroy:hover {
240 | color: #af5b5e;
241 | }
242 |
243 | .todo-list li .destroy:after {
244 | content: '×';
245 | }
246 |
247 | .todo-list li:hover .destroy {
248 | display: block;
249 | }
250 |
251 | .todo-list li .edit {
252 | display: none;
253 | }
254 |
255 | .todo-list li.editing:last-child {
256 | margin-bottom: -1px;
257 | }
258 |
259 | .footer {
260 | color: #777;
261 | padding: 10px 15px;
262 | height: 20px;
263 | text-align: center;
264 | border-top: 1px solid #e6e6e6;
265 | }
266 |
267 | .footer:before {
268 | content: '';
269 | position: absolute;
270 | right: 0;
271 | bottom: 0;
272 | left: 0;
273 | height: 50px;
274 | overflow: hidden;
275 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
276 | 0 8px 0 -3px #f6f6f6,
277 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
278 | 0 16px 0 -6px #f6f6f6,
279 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
280 | }
281 |
282 | .todo-count {
283 | float: left;
284 | text-align: left;
285 | }
286 |
287 | .todo-count strong {
288 | font-weight: 300;
289 | }
290 |
291 | .filters {
292 | margin: 0;
293 | padding: 0;
294 | list-style: none;
295 | position: absolute;
296 | right: 0;
297 | left: 0;
298 | }
299 |
300 | .filters li {
301 | display: inline;
302 | }
303 |
304 | .filters li a {
305 | color: inherit;
306 | margin: 3px;
307 | padding: 3px 7px;
308 | text-decoration: none;
309 | border: 1px solid transparent;
310 | border-radius: 3px;
311 | }
312 |
313 | .filters li a:hover {
314 | border-color: rgba(175, 47, 47, 0.1);
315 | }
316 |
317 | .filters li a.selected {
318 | border-color: rgba(175, 47, 47, 0.2);
319 | }
320 |
321 | .clear-completed,
322 | html .clear-completed:active {
323 | float: right;
324 | position: relative;
325 | line-height: 20px;
326 | text-decoration: none;
327 | cursor: pointer;
328 | }
329 |
330 | .clear-completed:hover {
331 | text-decoration: underline;
332 | }
333 |
334 | .info {
335 | margin: 65px auto 0;
336 | color: #bfbfbf;
337 | font-size: 10px;
338 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
339 | text-align: center;
340 | }
341 |
342 | .info p {
343 | line-height: 1;
344 | }
345 |
346 | .info a {
347 | color: inherit;
348 | text-decoration: none;
349 | font-weight: 400;
350 | }
351 |
352 | .info a:hover {
353 | text-decoration: underline;
354 | }
355 |
356 | /*
357 | Hack to remove background from Mobile Safari.
358 | Can't use it globally since it destroys checkboxes in Firefox
359 | */
360 | @media screen and (-webkit-min-device-pixel-ratio:0) {
361 | .toggle-all,
362 | .todo-list li .toggle {
363 | background: none;
364 | }
365 |
366 | .todo-list li .toggle {
367 | height: 40px;
368 | }
369 | }
370 |
371 | @media (max-width: 430px) {
372 | .footer {
373 | height: 50px;
374 | }
375 |
376 | .filters {
377 | bottom: 10px;
378 | }
379 | }
380 | hr {
381 | margin: 20px 0;
382 | border: 0;
383 | border-top: 1px dashed #c5c5c5;
384 | border-bottom: 1px dashed #f7f7f7;
385 | }
386 |
387 | .learn a {
388 | font-weight: normal;
389 | text-decoration: none;
390 | color: #b83f45;
391 | }
392 |
393 | .learn a:hover {
394 | text-decoration: underline;
395 | color: #787e7e;
396 | }
397 |
398 | .learn h3,
399 | .learn h4,
400 | .learn h5 {
401 | margin: 10px 0;
402 | font-weight: 500;
403 | line-height: 1.2;
404 | color: #000;
405 | }
406 |
407 | .learn h3 {
408 | font-size: 24px;
409 | }
410 |
411 | .learn h4 {
412 | font-size: 18px;
413 | }
414 |
415 | .learn h5 {
416 | margin-bottom: 0;
417 | font-size: 14px;
418 | }
419 |
420 | .learn ul {
421 | padding: 0;
422 | margin: 0 0 30px 25px;
423 | }
424 |
425 | .learn li {
426 | line-height: 20px;
427 | }
428 |
429 | .learn p {
430 | font-size: 15px;
431 | font-weight: 300;
432 | line-height: 1.3;
433 | margin-top: 0;
434 | margin-bottom: 0;
435 | }
436 |
437 | #issue-count {
438 | display: none;
439 | }
440 |
441 | .quote {
442 | border: none;
443 | margin: 20px 0 60px 0;
444 | }
445 |
446 | .quote p {
447 | font-style: italic;
448 | }
449 |
450 | .quote p:before {
451 | content: '“';
452 | font-size: 50px;
453 | opacity: .15;
454 | position: absolute;
455 | top: -20px;
456 | left: 3px;
457 | }
458 |
459 | .quote p:after {
460 | content: '”';
461 | font-size: 50px;
462 | opacity: .15;
463 | position: absolute;
464 | bottom: -42px;
465 | right: 3px;
466 | }
467 |
468 | .quote footer {
469 | position: absolute;
470 | bottom: -40px;
471 | right: 0;
472 | }
473 |
474 | .quote footer img {
475 | border-radius: 3px;
476 | }
477 |
478 | .quote footer a {
479 | margin-left: 5px;
480 | vertical-align: middle;
481 | }
482 |
483 | .speech-bubble {
484 | position: relative;
485 | padding: 10px;
486 | background: rgba(0, 0, 0, .04);
487 | border-radius: 5px;
488 | }
489 |
490 | .speech-bubble:after {
491 | content: '';
492 | position: absolute;
493 | top: 100%;
494 | right: 30px;
495 | border: 13px solid transparent;
496 | border-top-color: rgba(0, 0, 0, .04);
497 | }
498 |
499 | .learn-bar > .learn {
500 | position: absolute;
501 | width: 272px;
502 | top: 8px;
503 | left: -300px;
504 | padding: 10px;
505 | border-radius: 5px;
506 | background-color: rgba(255, 255, 255, .6);
507 | transition-property: left;
508 | transition-duration: 500ms;
509 | }
510 |
511 | @media (min-width: 899px) {
512 | .learn-bar {
513 | width: auto;
514 | padding-left: 300px;
515 | }
516 |
517 | .learn-bar > .learn {
518 | left: 8px;
519 | }
520 | }
--------------------------------------------------------------------------------
/examples/astro-react-todomvc/src/app/root.tsx:
--------------------------------------------------------------------------------
1 | import type { Todo } from '@prisma/client'
2 | import { server$, json } from '@tanstack/bling'
3 | import { useContext, useRef } from 'react'
4 | import {
5 | ActionFunctionArgs,
6 | Form,
7 | NavLink,
8 | RouteObject,
9 | useLoaderData,
10 | useSubmit,
11 | } from 'react-router-dom'
12 | import '../app.css'
13 | import { prisma } from './db.secret$'
14 | import invariant from 'tiny-invariant'
15 | import { manifestContext } from './manifest'
16 |
17 | function validateNewTodoTitle(title: string) {
18 | return title ? null : 'Todo title required'
19 | }
20 |
21 | const generalActions = {
22 | async createTodo({ formData, intent }) {
23 | const title = formData.get('title')
24 | invariant(typeof title === 'string', 'title must be a string')
25 | if (title.includes('error')) {
26 | return json(
27 | {
28 | type: 'error',
29 | intent,
30 | title,
31 | error: `Todos cannot include the word "error"`,
32 | },
33 | { status: 400 },
34 | )
35 | }
36 |
37 | const titleError = validateNewTodoTitle(title)
38 | if (titleError) {
39 | return json({ type: 'error', intent, error: titleError }, { status: 400 })
40 | }
41 | await prisma.todo.create({
42 | data: {
43 | complete: false,
44 | title: String(title),
45 | },
46 | })
47 | return json({ type: 'success', intent })
48 | },
49 | async toggleAllTodos({ formData, intent }) {
50 | await prisma.todo.updateMany({
51 | data: { complete: formData.get('complete') === 'true' },
52 | })
53 | return json({ type: 'success', intent })
54 | },
55 | async deleteCompletedTodos({ intent }) {
56 | await prisma.todo.deleteMany({ where: { complete: true } })
57 | return json({ type: 'success', intent })
58 | },
59 | } satisfies Record<
60 | string,
61 | (args: {
62 | formData: FormData
63 | intent: string
64 | }) => Promise<
65 | ReturnType<
66 | typeof json<
67 | | { type: 'success'; intent: string }
68 | | { type: 'error'; intent: string; error: string }
69 | >
70 | >
71 | >
72 | >
73 |
74 | const todoActions = {
75 | async toggleTodo({ todoId, formData, intent }) {
76 | await prisma.todo.update({
77 | where: { id: todoId },
78 | data: { complete: formData.get('complete') === 'true' },
79 | })
80 | return json({ type: 'success', intent })
81 | },
82 | async updateTodo({ formData, todoId, intent }) {
83 | const title = formData.get('title')
84 | invariant(typeof title === 'string', 'title must be a string')
85 | if (title.includes('error')) {
86 | return json(
87 | {
88 | type: 'error',
89 | intent,
90 | error: `Todos cannot include the word "error"`,
91 | },
92 | { status: 400 },
93 | )
94 | }
95 | const titleError = validateNewTodoTitle(title)
96 | if (titleError) {
97 | return json({ type: 'error', intent, error: titleError }, { status: 400 })
98 | }
99 |
100 | await prisma.todo.update({
101 | where: { id: todoId },
102 | data: { title },
103 | })
104 | return json({ type: 'success', intent })
105 | },
106 | async deleteTodo({ todoId, intent }) {
107 | await prisma.todo.delete({ where: { id: todoId } })
108 | return json({ type: 'success', intent })
109 | },
110 | } satisfies Record<
111 | string,
112 | (args: {
113 | formData: FormData
114 | todoId: string
115 | intent: string
116 | }) => Promise<
117 | ReturnType<
118 | typeof json<
119 | | { type: 'success'; intent: string }
120 | | { type: 'error'; error: string; intent: string }
121 | >
122 | >
123 | >
124 | >
125 |
126 | function hasKey>(
127 | obj: Obj,
128 | key: any,
129 | ): key is keyof Obj {
130 | return obj.hasOwnProperty(key)
131 | }
132 |
133 | function Scripts() {
134 | const manifest = useContext(manifestContext)
135 | return import.meta.env.DEV ? (
136 | <>
137 |
138 |
139 | >
140 | ) : (
141 |
142 | )
143 | }
144 |
145 | function Todomvc() {
146 | let todos = useLoaderData() as Todo[]
147 | console.log({ todos })
148 | let submit = useSubmit()
149 | const inputRef = useRef(null)
150 | return (
151 |
152 |
153 |
154 | Astro Bling • TodoMVC
155 |
156 |
157 |
268 |
281 |
282 |
283 |
284 | )
285 | }
286 |
287 | async function action({ request, params, context }: ActionFunctionArgs) {
288 | // debugger
289 | let entry = await request.formData()
290 |
291 | return await server$(async (formData, ctx) => {
292 | const intent = formData.get('intent')
293 |
294 | if (hasKey(generalActions, intent)) {
295 | return generalActions[intent]({ formData, intent })
296 | } else if (hasKey(todoActions, intent)) {
297 | const todoId = formData.get('todoId')
298 | invariant(typeof todoId === 'string', 'todoId must be a string')
299 | // make sure the todo belongs to the user
300 | const todo = await prisma.todo.findFirst({ where: { id: todoId } })
301 |
302 | if (!todo) {
303 | throw json({ error: 'todo not found' }, { status: 404 })
304 | }
305 | return todoActions[intent]({ formData, intent, todoId })
306 | } else {
307 | throw json({ error: `Unknown intent: ${intent}` }, { status: 400 })
308 | }
309 | })(entry)
310 | }
311 |
312 | export let routes = [
313 | {
314 | path: '/',
315 | loader: server$(async (args) => {
316 | return await prisma.todo.findMany()
317 | }),
318 | action,
319 | element: ,
320 | },
321 | {
322 | path: '/active',
323 | loader: server$(async (args) => {
324 | return await prisma.todo.findMany({
325 | where: {
326 | complete: false,
327 | },
328 | })
329 | }),
330 | action,
331 | element: ,
332 | },
333 | {
334 | path: '/completed',
335 | loader: server$(async (args) => {
336 | return await prisma.todo.findMany({
337 | where: {
338 | complete: true,
339 | },
340 | })
341 | }),
342 | action,
343 | element: ,
344 | },
345 | ] satisfies RouteObject[]
346 |
--------------------------------------------------------------------------------
/examples/astro-solid-router/src/app/data/Form.tsx:
--------------------------------------------------------------------------------
1 | /*!
2 | * Original code by Remix Sofware Inc
3 | * MIT Licensed, Copyright(c) 2021 Remix software Inc, see LICENSE.remix.md for details
4 | *
5 | * Credits to the Remix team for the Form implementation:
6 | * https://github.com/remix-run/remix/blob/main/packages/remix-react/components.tsx#L865
7 | */
8 | import { ComponentProps, createEffect, mergeProps, onCleanup, splitProps } from "solid-js";
9 |
10 | export interface FormAction {
11 | action: string;
12 | method: string;
13 | formData: Data;
14 | encType: string;
15 | }
16 |
17 | export { FormError } from "./FormError";
18 | export { FormImpl as Form };
19 |
20 | type FormEncType = "application/x-www-form-urlencoded" | "multipart/form-data";
21 |
22 | export interface SubmitOptions {
23 | /**
24 | * The HTTP method used to submit the form. Overrides `
195 | {story()!.comments_count 196 | ? story()!.comments_count + ' comments' 197 | : 'No comments yet.'} 198 |
199 |200 |
201 | {(comment) => }
202 |
203 |
204 |