TanStack Bling

repository·main·Indexed 23 days ago

https://github.com/tanstack/bling

Framework-agnostic transpilation utilities for simplifying complex patterns such as client/server RPCs, environment variable isolation, and code-splitting. It provides compiler macros including server$ for isomorphic RPCs, secret$ for server-only expressions, and import$ for build-time code-splitting. Includes a dedicated Astro integration via astroBling() and utilities for managing server-side handlers and custom serializers.

Tokens
4.2K
Snippets
8
Records
31
Agent score
79%

What's inside @tanstack/bling

  1. Use `.secret.[ext]` files for server-only code

    main

    You can create files that are strictly server-side by using the [filename].secret.[ext] naming convention (e.g., database.secret.ts).

    Behavior

    • Build-time: These files are removed from the client bundle entirely.
    • Client-side Imports: If a client-side file imports from a .secret. file, the exports will be provided as stubs with undefined values.

    Warning: Do not rely on the name of an exported variable for security, as the variable name itself is still visible in the client bundle, even if its value is undefined.

  2. Understand the Astro project structure

    main

    Astro projects follow a specific directory structure for routing and assets:

    • src/pages/: Contains .astro or .md files. Each file in this directory is exposed as a route based on its filename.
    • src/components/: A directory for storing Astro, React, Vue, Svelte, or Preact components.
    • src/layouts/: Typically used for defining page layouts.
    • public/: Contains static assets like images and favicons that are served directly.
    • package.json: Defines project dependencies and scripts.
    /
    ├── public/
    │   └── favicon.svg
    ├── src/
    │   ├── components/
    │   │   └── Card.astro
    │   ├── layouts/
    │   │   └── Layout.astro
    │   └── pages/
    │       └── index.astro
    └── package.json
  3. Run commands in an Astro project

    main

    Use the following npm commands from the root of your project to manage your Astro development lifecycle:

    • npm install: Installs project dependencies.
    • npm run dev: Starts a local development server at localhost:3000.
    • npm run build: Builds your production site into the ./dist/ directory.
    • npm run preview: Previews your production build locally before deploying.
    • npm run astro ...: Runs Astro CLI commands (e.g., npm run astro add).
    • npm run astro --help: Displays help information for the Astro CLI.
    npm install
    npm run dev
    npm run build
    npm run preview
  4. Run Astro commands

    main

    All commands are executed from the project root using npm. Use the following commands to manage your development lifecycle:

    • npm install: Installs project dependencies.
    • npm run dev: Starts the local development server at localhost:3000.
    • npm run build: Builds the production site into the ./dist/ directory.
    • npm run preview: Locally previews your production build before deployment.
    • npm run astro ...: Runs Astro CLI commands (e.g., npm run astro add).
    • npm run astro --help: Displays help information for the Astro CLI.
    npm install
    npm run dev
    npm run build
    npm run preview
  5. Understand the Fetcher type and its methods

    main

    A Fetcher is a specialized function used to perform data fetching. It combines a callable function with metadata and utility methods.

    When you call a Fetcher directly, it returns a Promise of the awaited return value of the underlying fetch function. If the underlying function returns a JsonResponse, the fetcher automatically unwraps it to return the data R instead of the full Response object.

    A Fetcher includes the following properties:

    • Direct Call: (payload, opts?) => Promise<Data>
    • url: A string representing the endpoint URL.
    • fetch(init: RequestInit, opts?: FetchFnCtxOptions): Promise<Data>: A method to perform the fetch using standard RequestInit options.
  6. Bling Macro Transformations

    main

    The Bling compiler recognizes and transforms several specific macros to enable advanced runtime behaviors:

    • fetch$(): Transforms RPC calls into server handlers or fetchers depending on the environment (SSR vs Client).
    • server$(): Transforms server-side logic into fetch$ calls.
    • import$(): Transforms server-only imports into dynamic import() calls.
    • split$(): Enables code splitting by transforming calls into dynamic imports pointing to virtual modules.
    • secret$(): Replaces expressions with undefined on the client, while preserving them during SSR.
    • lazy$(): Transforms expressions into React lazy() calls for deferred loading.
  7. Understand the ClientFetcher interface

    main

    A ClientFetcher is a specialized function returned by createFetcher. It combines the ability to be called directly with a payload and provides metadata and manual fetch capabilities.

    Key Properties/Methods:

    • (payload: any, opts?: FetchFnCtxOptions) => Promise<Awaited<FetchFnReturn<T>>>: Calling the fetcher directly with a payload.
    • .url: The pathname/route associated with the fetcher.
    • .fetch(init: RequestInit, opts?: FetchFnCtxOptions): A method to execute a request with specific RequestInit (like headers or body) and additional FetchFnCtxOptions.
  8. How server-side handlers and requests work together

    main

    Bling uses a routing mechanism based on pathnames to connect client-side calls to server-side functions:

    1. Registration: You define a function and register it at a specific pathname using fetch$.createHandler and fetch$.registerHandler.
    2. Client Call: When the client calls the resulting Fetcher, it sends a request to that pathname.
    3. Server Interception: The server-side handleFetch$ function intercepts the request, extracts the pathname and payload (from the body for POST/PUT or query params for GET), and looks up the registered handler.
    4. Execution: The handler executes, and the result is wrapped in a standard Web Response object with specific Bling headers (like XBlingResponseTypeHeader and XBlingContentTypeHeader) so the client knows how to parse it.
  9. Setup Bling in Astro using astroBling()

    main
    To use Bling within an Astro project, use the astroBling() integration function. This integration automatically configures Vite to include the bling() plugin and handles the necessary build configurations for client-side entry points (specifically looking for src/app/entry-client.tsx) during the Astro build process.
  10. Scope expressions to the server with `secret$`

    main

    The secret$ macro ensures that an expression is included in the server bundle but completely removed from the client bundle. This is the primary way to protect sensitive environment variables or prevent server-only imports from leaking into the client-side code.

    Note on Types: The return type of secret$ is the same as the input type. While the value will be undefined at runtime on the client, the type system treats it as the original type to maintain developer ergonomics.

    import { secret$ } from '@tanstack/bling'
    
    // The value is available on the server, but is 'undefined' on the client
    const secretMessage = secret$('It is a secret!')
  11. Code-split modules with `import$`

    main

    The import$ macro allows you to perform build-time code-splitting for any expression. It functions similarly to a native dynamic import(), but it compiles down to a dynamic import with a unique hash for each instance, allowing you to coordinate code-splitting without manually creating separate files.

    Common Use Cases

    • Dynamic Logic: Loading specific functions only when needed.
    • Framework Components: Integrating with lazy loading in frameworks like React or Solid.
    import { import$ } from '@tanstack/bling'
    
    // Code-splitting a function
    const fn = await import$(async (name: string) => {
      return `Hello ${name}`
    })
    
    // Code-splitting a React component
    import { lazy } from 'react'
    const LazyComponent = lazy(() => import$(
      {
        default: () => <div>Hello World!</div>,
      }
    ))