├── .changeset ├── README.md └── config.json ├── .github ├── ISSUE_TEMPLATE │ ├── Bug.md │ └── RFC.md └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── examples ├── ecommerce │ ├── .gitignore │ ├── .vscode │ │ └── settings.json │ ├── README.md │ ├── app │ │ ├── api │ │ │ └── fuse │ │ │ │ └── route.ts │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.module.css │ │ └── page.tsx │ ├── components │ │ ├── Cart.module.css │ │ ├── Cart.tsx │ │ ├── Category.module.css │ │ ├── Category.tsx │ │ ├── DatalayerProvider.tsx │ │ ├── Product.module.css │ │ └── Product.tsx │ ├── fuse │ │ ├── client.ts │ │ ├── fragment-masking.ts │ │ ├── gql.ts │ │ ├── graphql.ts │ │ ├── index.ts │ │ ├── pages.ts │ │ └── server.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── next.svg │ │ └── vercel.svg │ ├── schema.graphql │ ├── tsconfig.json │ └── types │ │ ├── Cart.ts │ │ ├── Category.ts │ │ ├── Product.ts │ │ └── context.ts ├── spacex │ ├── .gitignore │ ├── .vscode │ │ └── settings.json │ ├── README.md │ ├── app │ │ ├── api │ │ │ └── fuse │ │ │ │ └── route.ts │ │ ├── client │ │ │ ├── page.module.css │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.module.css │ │ ├── page.tsx │ │ └── rsc │ │ │ ├── page.module.css │ │ │ └── page.tsx │ ├── components │ │ ├── DatalayerProvider.tsx │ │ ├── LaunchDetails.tsx │ │ ├── LaunchItem.module.css │ │ ├── LaunchItem.tsx │ │ ├── LaunchSite.tsx │ │ ├── Location.tsx │ │ ├── PageNumbers.module.css │ │ ├── PageNumbers.tsx │ │ ├── Rocket.tsx │ │ └── actions │ │ │ └── sayHello.ts │ ├── fuse │ │ ├── client.ts │ │ ├── fragment-masking.ts │ │ ├── gql.ts │ │ ├── graphql.ts │ │ ├── index.ts │ │ ├── pages.ts │ │ └── server.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ └── test.tsx │ ├── public │ │ ├── next.svg │ │ └── vercel.svg │ ├── schema.graphql │ ├── tsconfig.json │ └── types │ │ ├── Launch.ts │ │ ├── launch │ │ ├── Rocket.ts │ │ └── Site.ts │ │ └── scopes.ts └── standalone │ ├── .gitignore │ ├── README.md │ ├── _context.ts │ ├── index.html │ ├── package.json │ ├── public │ └── vite.svg │ ├── schema.graphql │ ├── src │ ├── App.tsx │ ├── components │ │ ├── LaunchDetails.tsx │ │ ├── LaunchItem.module.css │ │ ├── LaunchItem.tsx │ │ ├── LaunchSite.tsx │ │ ├── Location.tsx │ │ ├── PageNumbers.module.css │ │ └── PageNumbers.tsx │ ├── fuse │ │ ├── index.ts │ │ ├── introspection.ts │ │ └── tada.ts │ ├── index.css │ ├── main.tsx │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── types │ ├── Launch.ts │ └── launch │ │ ├── Rocket.ts │ │ └── Site.ts │ └── vite.config.ts ├── package.json ├── packages ├── core │ ├── CHANGELOG.md │ ├── README.md │ ├── client.d.ts │ ├── loader.js │ ├── package.json │ ├── rsc.d.ts │ ├── src │ │ ├── adapters │ │ │ ├── bun.ts │ │ │ ├── cloudflare.ts │ │ │ ├── lambda.ts │ │ │ └── node.ts │ │ ├── builder.ts │ │ ├── cli.ts │ │ ├── client.ts │ │ ├── dev.ts │ │ ├── errors.ts │ │ ├── exchanges │ │ │ └── cache.ts │ │ ├── next │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── pages.ts │ │ │ ├── plugin.ts │ │ │ └── rsc.ts │ │ ├── pothos-list │ │ │ ├── global-types.ts │ │ │ ├── index.ts │ │ │ ├── schema-builder.ts │ │ │ └── types.ts │ │ └── utils │ │ │ ├── codegen.ts │ │ │ ├── gql-tada.ts │ │ │ └── yoga-helpers.ts │ ├── test │ │ ├── authorization.test.ts │ │ ├── cli.test.ts │ │ ├── errors.test.ts │ │ ├── fixtures │ │ │ ├── simple │ │ │ │ ├── package.json │ │ │ │ ├── tsconfig.json │ │ │ │ └── types │ │ │ │ │ └── Test.ts │ │ │ └── tada │ │ │ │ ├── package.json │ │ │ │ ├── tsconfig.json │ │ │ │ └── types │ │ │ │ └── Test.ts │ │ ├── list.test.ts │ │ ├── node.test.ts │ │ └── schema.test.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.mts └── create-fuse-app │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ ├── get-package-manager.ts │ ├── index.ts │ ├── install-package.ts │ └── rewrite-next.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── website ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .svgrrc.js ├── README.md ├── next-env.d.ts ├── next-sitemap.config.js ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── favicon.ico ├── favicon.png ├── images │ ├── data-layers-vs-api-gateways.png │ ├── data-layers-vs-bffs.png │ ├── data-layers-vs-graphql-federation.png │ ├── fuse-circle.svg │ ├── fuse-circles-with-logos.svg │ ├── fuse-circles-with-logos.webp │ ├── fuse-diagram-desktop.svg │ ├── fuse-diagram-mobile.svg │ ├── fuse-grid-logo.webp │ ├── fuse-logo-inverted.svg │ ├── fuse-logo-white-border.svg │ ├── fuse-logo.svg │ ├── fuse-outline.svg │ ├── fuse-workflow.svg │ ├── gql-with-circles.svg │ ├── homepage-code-sample-desktop.svg │ ├── homepage-code-sample-mobile.svg │ ├── nextjs-logo.svg │ ├── og-image.png │ └── the-grid.svg ├── next.svg ├── robots.txt ├── sitemap-0.xml ├── sitemap.xml ├── vercel.svg └── videos │ ├── video-poster.png │ ├── video-sample-vertical.mp4 │ ├── video-sample.mp4 │ └── video-vertical-poster.png ├── src ├── components │ ├── Button.tsx │ ├── Card.tsx │ ├── Explain.tsx │ ├── ExternalLink.tsx │ ├── HeadMeta.tsx │ ├── Heading.tsx │ ├── MobileMenuLines.tsx │ ├── PageVerticaLines.tsx │ ├── PoweredByCards.tsx │ ├── Section.tsx │ ├── StarOnGithub.tsx │ ├── Testimonials.tsx │ ├── Text.tsx │ ├── TheGrid.tsx │ ├── icons │ │ ├── AngularjsLogo.tsx │ │ ├── ArrowConnectingNodes.tsx │ │ ├── ArrowOpeningPath.tsx │ │ ├── Automatic.tsx │ │ ├── AwsLogo.tsx │ │ ├── BuildingBlock.tsx │ │ ├── BunLogo.tsx │ │ ├── CloudflareWorkersLogo.tsx │ │ ├── External.tsx │ │ ├── FuseLogo.tsx │ │ ├── FuseLogoInverted.tsx │ │ ├── FuseLogoWithName.tsx │ │ ├── FuseLogoWithNameDark.tsx │ │ ├── FuseLogoWithNameLight.tsx │ │ ├── GatsbyLogo.tsx │ │ ├── GithubLogo.tsx │ │ ├── GlobalUnique.tsx │ │ ├── GraphQlCodeGenLogo.tsx │ │ ├── GraphQlYogaLogo.tsx │ │ ├── GraphiqlLogo.tsx │ │ ├── GraphqlLogoOutline.tsx │ │ ├── HttpLogo.tsx │ │ ├── JavaLogo.tsx │ │ ├── KotlinLogo.tsx │ │ ├── NextJsLogo.tsx │ │ ├── NodeJsLogo.tsx │ │ ├── NodeStack.tsx │ │ ├── NpmLogo.tsx │ │ ├── Observability.tsx │ │ ├── PothosLogo.tsx │ │ ├── PrismaLogo.tsx │ │ ├── PuzzlePieces.tsx │ │ ├── ReactLogo.tsx │ │ ├── ReactNativeLogo.tsx │ │ ├── Relay.tsx │ │ ├── Scalable.tsx │ │ ├── Security.tsx │ │ ├── Star.tsx │ │ ├── StarSparkle.tsx │ │ ├── StellateLogo.tsx │ │ ├── StellateLogoWithName.tsx │ │ ├── SwiftLogo.tsx │ │ ├── Terminal.tsx │ │ ├── UrqlLogo.tsx │ │ ├── VueLogo.tsx │ │ ├── XLogo.tsx │ │ ├── index.ts │ │ └── svg │ │ │ ├── angularjsLogo.svg │ │ │ ├── arrowConnectingNodes.svg │ │ │ ├── arrowOpeningPath.svg │ │ │ ├── automatic.svg │ │ │ ├── awsLogo.svg │ │ │ ├── buildingBlock.svg │ │ │ ├── bunLogo.svg │ │ │ ├── cloudflareWorkersLogo.svg │ │ │ ├── external.svg │ │ │ ├── fuseLogo.svg │ │ │ ├── fuseLogoInverted.svg │ │ │ ├── fuseLogoWithName.svg │ │ │ ├── fuseLogoWithNameDark.svg │ │ │ ├── fuseLogoWithNameLight.svg │ │ │ ├── gatsbyLogo.svg │ │ │ ├── githubLogo.svg │ │ │ ├── globalUnique.svg │ │ │ ├── graphQLCodeGenLogo.svg │ │ │ ├── graphQLYogaLogo.svg │ │ │ ├── graphiqlLogo.svg │ │ │ ├── graphqlLogoOutline.svg │ │ │ ├── httpLogo.svg │ │ │ ├── javaLogo.svg │ │ │ ├── kotlinLogo.svg │ │ │ ├── nextJsLogo.svg │ │ │ ├── nodeJsLogo.svg │ │ │ ├── nodeStack.svg │ │ │ ├── npmLogo.svg │ │ │ ├── observability.svg │ │ │ ├── pothosLogo.svg │ │ │ ├── prismaLogo.svg │ │ │ ├── puzzlePieces.svg │ │ │ ├── reactLogo.svg │ │ │ ├── reactNativeLogo.svg │ │ │ ├── relay.svg │ │ │ ├── scalable.svg │ │ │ ├── security.svg │ │ │ ├── star.svg │ │ │ ├── starSparkle.svg │ │ │ ├── stellateLogo.svg │ │ │ ├── stellateLogoWithName.svg │ │ │ ├── swiftLogo.svg │ │ │ ├── terminal.svg │ │ │ ├── urqlLogo.svg │ │ │ ├── vueLogo.svg │ │ │ └── xLogo.svg │ └── snippets │ │ ├── client.mdx │ │ └── node.mdx ├── mdx.d.ts ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── _meta.json │ ├── docs │ │ ├── _meta.json │ │ ├── client │ │ │ ├── _meta.json │ │ │ ├── best-practices.mdx │ │ │ ├── index.mdx │ │ │ ├── javascript │ │ │ │ ├── _meta.json │ │ │ │ ├── angular.mdx │ │ │ │ ├── nextjs.mdx │ │ │ │ ├── react.mdx │ │ │ │ └── vue.mdx │ │ │ ├── other │ │ │ │ ├── _meta.json │ │ │ │ ├── android.mdx │ │ │ │ ├── http.mdx │ │ │ │ └── ios.mdx │ │ │ └── types.mdx │ │ ├── deployment │ │ │ ├── _meta.json │ │ │ ├── bun.mdx │ │ │ ├── cloudflare-workers.mdx │ │ │ ├── index.mdx │ │ │ ├── lambda.mdx │ │ │ ├── nextjs.mdx │ │ │ └── node.mdx │ │ ├── fuse-method │ │ │ ├── _meta.json │ │ │ ├── index.mdx │ │ │ ├── vs-backend-for-frontends.mdx │ │ │ └── vs-graphql-federation.mdx │ │ ├── index.mdx │ │ ├── server │ │ │ ├── _meta.json │ │ │ ├── authorization.mdx │ │ │ ├── context.mdx │ │ │ ├── error-handling.mdx │ │ │ ├── integrations │ │ │ │ ├── _meta.json │ │ │ │ ├── drizzle.mdx │ │ │ │ ├── grpc.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── kysely.mdx │ │ │ │ ├── prisma.mdx │ │ │ │ └── rest.mdx │ │ │ ├── lists.mdx │ │ │ ├── mocking.mdx │ │ │ ├── nodes.mdx │ │ │ ├── objects-enums-unions-interfaces.mdx │ │ │ └── queries-and-mutations.mdx │ │ ├── setting-fuse-up-manually │ │ │ ├── _meta.json │ │ │ ├── index.mdx │ │ │ └── nextjs.mdx │ │ └── troubleshooting.mdx │ └── index.tsx ├── styles │ └── globals.scss ├── svg.d.ts └── utils │ └── tailwind.ts ├── tailwind.config.ts ├── theme.config.tsx └── tsconfig.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "access": "public", 6 | "baseBranch": "main", 7 | "updateInternalDependencies": "minor", 8 | "ignore": ["@fuse-examples/*", "@fuse/website", "@fuse-fixtures/*"], 9 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { 10 | "onlyUpdatePeerDependentsWhenOutOfRange": true, 11 | "updateInternalDependents": "out-of-range" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'bug report' 3 | about: Report a bug with Fuse 4 | title: 'Bug: short description' 5 | labels: 'Bug' 6 | --- 7 | 8 | 9 | 10 | ## Description 11 | 12 | 13 | 14 | 15 | 16 | [Reproduction]() 17 | 18 | ## Versions 19 | 20 | - TypeScript Version: 21 | - Fuse version: 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/RFC.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'RFC' 3 | about: Propose an enhancement / feature and start a discussion 4 | title: 'RFC: Your Proposal' 5 | labels: 'RFC' 6 | --- 7 | 8 | ## Summary 9 | 10 | 16 | 17 | ## Proposed Solution 18 | 19 | 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | release: 9 | name: CI 10 | runs-on: ubuntu-20.04 11 | timeout-minutes: 20 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Setup Node 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 20 22 | 23 | - name: Setup pnpm 24 | uses: pnpm/action-setup@v2.2.4 25 | with: 26 | version: 8 27 | run_install: false 28 | 29 | - name: Get pnpm store directory 30 | id: pnpm-store 31 | run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT 32 | 33 | - name: Use pnpm store 34 | uses: actions/cache@v3 35 | id: pnpm-cache 36 | with: 37 | path: | 38 | ~/.cache/Cypress 39 | ${{ steps.pnpm-store.outputs.pnpm_cache_dir }} 40 | key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} 41 | restore-keys: | 42 | ${{ runner.os }}-pnpm- 43 | 44 | - name: Install Dependencies 45 | run: pnpm install 46 | 47 | - name: TypeCheck fuse 48 | id: typecheck-core 49 | run: pnpm --filter fuse typecheck 50 | 51 | - name: Build fuse 52 | id: build-core 53 | run: pnpm --filter fuse build 54 | 55 | - name: Test fuse 56 | id: test-core 57 | run: pnpm --filter fuse test 58 | 59 | - name: TypeCheck example 60 | id: typecheck-example 61 | run: pnpm --filter @fuse-examples/spacex typecheck 62 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-20.04 10 | timeout-minutes: 20 11 | permissions: 12 | contents: write 13 | id-token: write 14 | issues: write 15 | repository-projects: write 16 | deployments: write 17 | packages: write 18 | pull-requests: write 19 | steps: 20 | - name: Checkout Repo 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Setup Node 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: 20 29 | 30 | - name: Setup pnpm 31 | uses: pnpm/action-setup@v2.2.4 32 | with: 33 | version: 8 34 | run_install: false 35 | 36 | - name: Get pnpm store directory 37 | id: pnpm-store 38 | run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT 39 | 40 | - name: Use pnpm store 41 | uses: actions/cache@v3 42 | id: pnpm-cache 43 | with: 44 | path: | 45 | ~/.cache/Cypress 46 | ${{ steps.pnpm-store.outputs.pnpm_cache_dir }} 47 | key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} 48 | restore-keys: | 49 | ${{ runner.os }}-pnpm- 50 | 51 | - name: Install Dependencies 52 | run: pnpm install 53 | 54 | - name: PR or Publish 55 | id: changesets 56 | uses: changesets/action@v1.4.5 57 | with: 58 | version: pnpm changeset version 59 | publish: pnpm changeset publish 60 | env: 61 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | packages/core/test/fixtures/**/build 4 | packages/core/test/fixtures/**/schema.graphql -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | examples/spacex/gql 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Stellate 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fuse 2 | 3 | ![Fuse: End-to-end typesafe data fetching for frontend teams at scale](https://images.ctfassets.net/yq1dddfl2vc7/6EDzUh3emBY3uQqoxulmPA/c738d8fbae3e412e38cadee598f3e9db/twitter_header.png) 4 | 5 | # Getting Started 6 | 7 | When you are in the root of your app run the following command. This will 8 | install all the packages and generate the files you need. 9 | 10 | ```sh 11 | npx create-fuse-app 12 | ``` 13 | 14 | Then, run `npx fuse dev` and your API will be running at `localhost:4000/graphql`! 15 | 16 | > If you are **using Next.js, you don't need to manually run `fuse dev`**. `create-fuse-app` will add a Next.js plugin to your `next.config.js/ts/mjs`` and an API route at `/api/fuse` for you to access your API. ([learn more](https://fusedata.dev/docs/setting-fuse-up-manually/nextjs)) 17 | 18 | ## Querying your data layer 19 | 20 | ```tsx 21 | import { graphql } from '@/fuse' 22 | import { execute } from '@/fuse/server' 23 | 24 | const UserQuery = graphql(` 25 | query User($id: ID!) { 26 | user(id: $id) { 27 | id 28 | name 29 | } 30 | } 31 | `) 32 | 33 | export default async function Page() { 34 | const result = await execute({ 35 | query: UserQuery, 36 | variables: { id: '1' }, 37 | }) 38 | 39 | return

Welcome {result.data?.user?.name}

40 | } 41 | ``` 42 | 43 | # [Docs](https://fusedata.dev/docs) 44 | 45 | **Read [the documentation](https://fusedata.dev/docs) for more information about using Fuse**. 46 | 47 | Quicklinks to some of the most-visited pages: 48 | 49 | - [Getting started](https://fusedata.dev/docs) 50 | - [Querying your API (client)](https://fusedata.dev/docs/client) 51 | - [Building your API (server)](https://fusedata.dev/docs/server/queries-and-mutations) 52 | - [Deploying your API (server)](https://fusedata.dev/docs/deployment) 53 | - [The Fuse Method](https://fusedata.dev/docs/fuse-method) 54 | 55 | # License 56 | 57 | Licensed under the MIT License, Copyright © 2023-present Stellate, Inc. 58 | 59 | See LICENSE for more information. 60 | -------------------------------------------------------------------------------- /examples/ecommerce/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/ecommerce/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /examples/ecommerce/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/ecommerce/app/api/fuse/route.ts: -------------------------------------------------------------------------------- 1 | import { createAPIRouteHandler } from 'fuse/next' 2 | 3 | const layer = createAPIRouteHandler<{ userId: string }>({ 4 | context: (ctx) => ({ 5 | // For demo purposes everyone is the same user 6 | userId: '1', 7 | }), 8 | }) 9 | 10 | export const GET = layer 11 | export const POST = layer 12 | -------------------------------------------------------------------------------- /examples/ecommerce/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StellateHQ/fuse/26ea459f0075fa083734033c96014d37b762089a/examples/ecommerce/app/favicon.ico -------------------------------------------------------------------------------- /examples/ecommerce/app/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | max-width: 100vw; 4 | overflow-x: hidden; 5 | } 6 | 7 | body { 8 | color: black; 9 | } 10 | 11 | a { 12 | color: inherit; 13 | text-decoration: none; 14 | } 15 | -------------------------------------------------------------------------------- /examples/ecommerce/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from 'next/font/google' 2 | import './globals.css' 3 | import { DatalayerProvider } from '@/components/DatalayerProvider' 4 | 5 | const inter = Inter({ subsets: ['latin'] }) 6 | 7 | export default function RootLayout({ 8 | children, 9 | }: { 10 | children: React.ReactNode 11 | }) { 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /examples/ecommerce/app/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | min-height: 100vh; 6 | } 7 | 8 | .list { 9 | padding: 0; 10 | margin: 0; 11 | } 12 | -------------------------------------------------------------------------------- /examples/ecommerce/app/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import styles from './page.module.css' 4 | import { graphql } from '@/fuse' 5 | import { execute } from '@/fuse/server' 6 | import { Category } from '@/components/Category' 7 | import { Cart } from '@/components/Cart' 8 | 9 | const HomePageQuery = graphql(` 10 | query HomePage { 11 | myCart { 12 | ...Cart_CartFields 13 | } 14 | categories { 15 | ...Category_CategoryFields 16 | } 17 | } 18 | `) 19 | 20 | export default async function Page() { 21 | const result = await execute({ 22 | query: HomePageQuery, 23 | variables: {}, 24 | context: () => ({ userId: '1' }), 25 | }) 26 | return ( 27 |
28 |

Fuse Store

29 | {result.data?.myCart && } 30 | {result.data?.categories.map((category, i) => ( 31 | 32 | ))} 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /examples/ecommerce/components/Cart.module.css: -------------------------------------------------------------------------------- 1 | .cartTitle { 2 | margin-bottom: 12px; 3 | text-align: center; 4 | } 5 | 6 | .cartSection { 7 | margin-bottom: 16px; 8 | } 9 | 10 | .grid { 11 | list-style: none; 12 | display: grid; 13 | grid-template-columns: 1fr 1fr 1fr; 14 | gap: 16px; 15 | padding: 0; 16 | margin: 0; 17 | } -------------------------------------------------------------------------------- /examples/ecommerce/components/Cart.tsx: -------------------------------------------------------------------------------- 1 | import { FragmentType, graphql, useFragment } from '@/fuse' 2 | import { Product } from './Product' 3 | import styles from './Cart.module.css' 4 | 5 | const CartFields = graphql(` 6 | fragment Cart_CartFields on Cart { 7 | items { 8 | quantity 9 | product { 10 | id 11 | price 12 | ...Product_ProductFields 13 | } 14 | } 15 | } 16 | `) 17 | 18 | export const Cart = (props: { cart: FragmentType }) => { 19 | const cart = useFragment(CartFields, props.cart) 20 | return ( 21 |
22 |

23 | Cart - Total Price: $ 24 | {cart.items?.reduce( 25 | (acc, item) => acc + item.quantity * item.product.price, 26 | 0, 27 | )} 28 |

29 | 42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /examples/ecommerce/components/Category.module.css: -------------------------------------------------------------------------------- 1 | .categoryTitle { 2 | margin-bottom: 12px; 3 | text-align: center; 4 | } 5 | 6 | .categorySection { 7 | margin-bottom: 16px; 8 | } 9 | 10 | .grid { 11 | list-style: none; 12 | display: grid; 13 | grid-template-columns: 1fr 1fr 1fr; 14 | gap: 16px; 15 | padding: 0; 16 | margin: 0; 17 | } -------------------------------------------------------------------------------- /examples/ecommerce/components/Category.tsx: -------------------------------------------------------------------------------- 1 | import { FragmentType, graphql, useFragment } from '@/fuse' 2 | import { Product } from './Product' 3 | import styles from './Category.module.css' 4 | 5 | const CategoryFields = graphql(` 6 | fragment Category_CategoryFields on Category { 7 | name 8 | products { 9 | id 10 | ...Product_ProductFields 11 | } 12 | } 13 | `) 14 | 15 | export const Category = (props: { 16 | category: FragmentType 17 | }) => { 18 | const category = useFragment(CategoryFields, props.category) 19 | return ( 20 |
21 |

{category.name}

22 |
    23 | {category.products.map((product, i) => ( 24 | 25 | ))} 26 |
27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /examples/ecommerce/components/DatalayerProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Provider, createClient } from '@/fuse/client' 4 | import React, { Suspense } from 'react' 5 | 6 | export const DatalayerProvider = (props: any) => { 7 | const [client, ssr] = React.useMemo(() => { 8 | const { client, ssr } = createClient({ 9 | url: 10 | process.env.NODE_ENV === 'production' 11 | ? 'https://spacex-fuse.vercel.app/api/fuse' 12 | : 'http://localhost:3000/api/fuse', 13 | }) 14 | 15 | return [client, ssr] 16 | }, []) 17 | 18 | return ( 19 | 20 | {props.children} 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /examples/ecommerce/components/Product.module.css: -------------------------------------------------------------------------------- 1 | .productItem { 2 | border: 1px solid black; 3 | border-radius: 6px; 4 | padding: 16px; 5 | width: 256px; 6 | display: flex; 7 | flex-direction: column; 8 | } 9 | 10 | .addToCart { 11 | margin-bottom: 16px; 12 | } 13 | 14 | .productImage { 15 | height: 128px; 16 | width: 128px; 17 | margin-left: auto; 18 | margin-right: auto; 19 | margin-bottom: 12px; 20 | } 21 | 22 | .productName { 23 | text-align: center; 24 | margin: 0; 25 | margin-bottom: 16px; 26 | } 27 | 28 | .productDescription { 29 | margin: 0; 30 | font-size: 14px; 31 | } 32 | -------------------------------------------------------------------------------- /examples/ecommerce/components/Product.tsx: -------------------------------------------------------------------------------- 1 | 'use client;' 2 | 3 | import { FragmentType, graphql, useFragment } from '@/fuse' 4 | import styles from './Product.module.css' 5 | 6 | const ProductFields = graphql(` 7 | fragment Product_ProductFields on Product { 8 | id 9 | name 10 | image 11 | description 12 | price 13 | } 14 | `) 15 | 16 | export const Product = (props: { 17 | product: FragmentType 18 | noAddToCart?: boolean 19 | }) => { 20 | const product = useFragment(ProductFields, props.product) 21 | return ( 22 |
  • 23 | {product.name} 30 |

    31 | {product.name} - ${product.price} 32 |

    33 | {!props.noAddToCart && ( 34 | 35 | )} 36 | {product.description && ( 37 |

    {product.description}

    38 | )} 39 |
  • 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /examples/ecommerce/fuse/client.ts: -------------------------------------------------------------------------------- 1 | // This is a generated file! 2 | 3 | export * from 'fuse/next/client' 4 | -------------------------------------------------------------------------------- /examples/ecommerce/fuse/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fragment-masking' 2 | export * from './gql' 3 | -------------------------------------------------------------------------------- /examples/ecommerce/fuse/pages.ts: -------------------------------------------------------------------------------- 1 | // This is a generated file! 2 | 3 | export * from 'fuse/next/pages' 4 | -------------------------------------------------------------------------------- /examples/ecommerce/fuse/server.ts: -------------------------------------------------------------------------------- 1 | // This is a generated file! 2 | 3 | export * from 'fuse/next/server' 4 | export { __internal_execute as execute } from 'fuse/next/server' 5 | -------------------------------------------------------------------------------- /examples/ecommerce/next.config.js: -------------------------------------------------------------------------------- 1 | const { nextFusePlugin } = require('fuse/next/plugin') 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = nextFusePlugin({})({}) 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /examples/ecommerce/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fuse-examples/ecommerce", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "typecheck": "tsc" 11 | }, 12 | "dependencies": { 13 | "@graphql-typed-document-node/core": "^3.2.0", 14 | "graphql": "^16.8.1", 15 | "next": "14.0.3", 16 | "react": "^18", 17 | "react-dom": "^18" 18 | }, 19 | "devDependencies": { 20 | "@0no-co/graphqlsp": "^1.0.2", 21 | "@types/node": "^20", 22 | "@types/react": "^18", 23 | "@types/react-dom": "^18", 24 | "@types/webpack-env": "^1.18.4", 25 | "fuse": "workspace:*", 26 | "typescript": "^5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/ecommerce/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/ecommerce/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/ecommerce/schema.graphql: -------------------------------------------------------------------------------- 1 | type Cart { 2 | id: ID 3 | items: [CartItem!] 4 | } 5 | 6 | type CartItem { 7 | product: Product! 8 | quantity: Int! 9 | } 10 | 11 | type Category { 12 | name: String! 13 | products: [Product!]! 14 | } 15 | 16 | """ 17 | A date string, such as 2007-12-03, compliant with the `full-date` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. 18 | """ 19 | scalar Date 20 | 21 | """ 22 | The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). 23 | """ 24 | scalar JSON 25 | 26 | type Mutation { 27 | _version: String! 28 | addToCart(productId: ID!, quantity: Int = 1): Cart 29 | } 30 | 31 | interface Node { 32 | id: ID! 33 | } 34 | 35 | type Product implements Node { 36 | description: String 37 | id: ID! 38 | image: String! 39 | name: String! 40 | price: Float! 41 | } 42 | 43 | type Query { 44 | _version: String! 45 | categories: [Category!]! 46 | myCart: Cart 47 | node(id: ID!): Node 48 | nodes(ids: [ID!]!): [Node]! 49 | product(id: ID!): Product 50 | } -------------------------------------------------------------------------------- /examples/ecommerce/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "Bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "types": [ 17 | "webpack-env" // here 18 | ], 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | }, 23 | { 24 | "name": "@0no-co/graphqlsp", 25 | "schema": "./schema.graphql", 26 | "disableTypegen": true, 27 | "templateIsCallExpression": true, 28 | "template": "graphql" 29 | } 30 | ], 31 | "paths": { 32 | "@/*": ["./*"] 33 | } 34 | }, 35 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 36 | "exclude": ["node_modules"] 37 | } 38 | -------------------------------------------------------------------------------- /examples/ecommerce/types/Cart.ts: -------------------------------------------------------------------------------- 1 | import { addMutationFields, addQueryFields, objectType } from 'fuse' 2 | import { ProductNode } from './Product' 3 | 4 | type CartItem = { 5 | productId: number 6 | quantity: number 7 | } 8 | 9 | interface Cart { 10 | id: number 11 | products: Array 12 | } 13 | 14 | const CartItemObject = objectType({ 15 | name: 'CartItem', 16 | fields: (t) => ({ 17 | quantity: t.exposeInt('quantity', { nullable: false }), 18 | product: t.field({ 19 | nullable: false, 20 | type: ProductNode, 21 | // We only return the product id here and let the ProductNode 22 | // automatically resolve everything 23 | resolve: (p) => p.productId, 24 | }), 25 | }), 26 | }) 27 | 28 | const CartObject = objectType({ 29 | name: 'Cart', 30 | fields: (t) => ({ 31 | id: t.exposeID('id'), 32 | items: t.field({ 33 | type: [CartItemObject], 34 | resolve: (parent) => parent.products, 35 | }), 36 | }), 37 | }) 38 | 39 | addMutationFields((t) => ({ 40 | addToCart: t.field({ 41 | type: CartObject, 42 | args: { 43 | productId: t.arg.id({ required: true }), 44 | quantity: t.arg.int({ defaultValue: 1 }), 45 | }, 46 | resolve: async (_, args, ctx) => { 47 | const result = await fetch('https://fakestoreapi.com/carts', { 48 | method: 'POST', 49 | body: JSON.stringify({ 50 | userId: ctx.userId, 51 | date: '2020-01-02', 52 | products: [{ productId: args.productId, quantity: args.quantity }], 53 | }), 54 | }).then((res) => res.json()) 55 | 56 | return { 57 | ...result, 58 | products: [{ productId: args.productId, quantity: args.quantity }], 59 | } 60 | }, 61 | }), 62 | })) 63 | 64 | addQueryFields((t) => ({ 65 | myCart: t.field({ 66 | type: CartObject, 67 | resolve: async (_, __, ctx) => { 68 | const carts = await fetch( 69 | 'https://fakestoreapi.com/carts/user/' + ctx.userId, 70 | ).then((x) => x.json()) 71 | return carts[0] 72 | }, 73 | }), 74 | })) 75 | -------------------------------------------------------------------------------- /examples/ecommerce/types/Category.ts: -------------------------------------------------------------------------------- 1 | import { addQueryFields, objectType } from 'fuse' 2 | 3 | export const CategoryObject = objectType<{ name: string }>({ 4 | name: 'Category', 5 | fields: (t) => ({ 6 | name: t.exposeString('name', { nullable: false }), 7 | }), 8 | }) 9 | 10 | addQueryFields((t) => ({ 11 | categories: t.field({ 12 | type: [CategoryObject], 13 | nullable: false, 14 | resolve: async (_, args) => { 15 | const categories = await fetch( 16 | `https://fakestoreapi.com/products/categories`, 17 | ).then((x) => x.json()) 18 | 19 | return categories.map((name: string) => ({ name })) 20 | }, 21 | }), 22 | })) 23 | -------------------------------------------------------------------------------- /examples/ecommerce/types/Product.ts: -------------------------------------------------------------------------------- 1 | import { addObjectFields, node } from 'fuse' 2 | import { CategoryObject } from './Category' 3 | 4 | interface Product { 5 | id: number 6 | title: string 7 | price: number 8 | description: string 9 | image: string 10 | } 11 | 12 | export const ProductNode = node({ 13 | name: 'Product', 14 | load: (ids) => getProducts(ids), 15 | fields: (t) => ({ 16 | name: t.exposeString('title', { nullable: false }), 17 | price: t.exposeFloat('price', { nullable: false }), 18 | description: t.exposeString('description'), 19 | image: t.exposeString('image', { nullable: false }), 20 | }), 21 | }) 22 | 23 | addObjectFields(CategoryObject, (t) => ({ 24 | products: t.loadableList({ 25 | nullable: false, 26 | type: ProductNode, 27 | resolve: (parent) => { 28 | return parent.name 29 | }, 30 | load: async (keys) => { 31 | const result = await Promise.allSettled( 32 | keys.map((key) => 33 | fetch(`https://fakestoreapi.com/products/category/` + key).then((x) => 34 | x.json(), 35 | ), 36 | ), 37 | ) 38 | return result.map((x) => 39 | x.status === 'fulfilled' ? x.value : new Error(x.reason), 40 | ) 41 | }, 42 | }), 43 | })) 44 | 45 | async function getProducts(ids: (number | string)[]) { 46 | const result = await Promise.allSettled( 47 | ids.map((id) => 48 | fetch(`https://fakestoreapi.com/products/` + id).then((x) => x.json()), 49 | ), 50 | ) 51 | return result.map((x) => 52 | x.status === 'fulfilled' ? x.value : new Error(x.reason), 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /examples/ecommerce/types/context.ts: -------------------------------------------------------------------------------- 1 | import 'fuse' 2 | 3 | declare module 'fuse' { 4 | export interface UserContext { 5 | userId: string 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/spacex/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/spacex/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /examples/spacex/README.md: -------------------------------------------------------------------------------- 1 | # SpaceX 2 | 3 | This API can be run locally with `pnpm dev` or can be viewed [as deployed here](https://spacex-fuse.vercel.app/). 4 | -------------------------------------------------------------------------------- /examples/spacex/app/api/fuse/route.ts: -------------------------------------------------------------------------------- 1 | import { createAPIRouteHandler } from 'fuse/next' 2 | 3 | const layer = createAPIRouteHandler({ 4 | context: (req) => ({ user: true }), 5 | }) 6 | 7 | export const GET = layer 8 | export const POST = layer 9 | -------------------------------------------------------------------------------- /examples/spacex/app/client/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | min-height: 100vh; 6 | } 7 | 8 | .list { 9 | padding: 0; 10 | margin: 0; 11 | } 12 | -------------------------------------------------------------------------------- /examples/spacex/app/client/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { graphql } from '@/fuse' 6 | import { LaunchItem } from '@/components/LaunchItem' 7 | import { LaunchDetails } from '@/components/LaunchDetails' 8 | import styles from './page.module.css' 9 | import { PageNumbers } from '@/components/PageNumbers' 10 | 11 | import { useQuery } from '@/fuse/client' 12 | import { useSearchParams } from 'next/navigation' 13 | 14 | export default function Page() { 15 | return ( 16 |
    17 |

    SpaceX Launches

    18 | Loading launches...

    }> 19 | 20 |
    21 |
    22 | ) 23 | } 24 | 25 | const LaunchesQuery = graphql(` 26 | query Launches_SSR($limit: Int, $offset: Int) { 27 | launches(limit: $limit, offset: $offset) { 28 | nodes { 29 | id 30 | ...LaunchFields 31 | } 32 | ...TotalCountFields 33 | } 34 | } 35 | `) 36 | 37 | function Launches() { 38 | const searchparams = useSearchParams() 39 | 40 | const selected = searchparams!.get('selected') 41 | const offset = searchparams!.has('offset') 42 | ? Number(searchparams!.get('offset')) 43 | : 0 44 | 45 | const [result] = useQuery({ 46 | query: LaunchesQuery, 47 | variables: { limit: 10, offset }, 48 | }) 49 | 50 | return ( 51 | <> 52 |
      53 | {result.data?.launches.nodes.map( 54 | (node) => node && , 55 | )} 56 |
    57 | {result.data && ( 58 | 59 | )} 60 | Loading details...

    }> 61 | {selected && } 62 |
    63 | 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /examples/spacex/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StellateHQ/fuse/26ea459f0075fa083734033c96014d37b762089a/examples/spacex/app/favicon.ico -------------------------------------------------------------------------------- /examples/spacex/app/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | max-width: 100vw; 4 | overflow-x: hidden; 5 | } 6 | 7 | body { 8 | color: black; 9 | } 10 | 11 | a { 12 | color: inherit; 13 | text-decoration: none; 14 | } 15 | -------------------------------------------------------------------------------- /examples/spacex/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from 'next/font/google' 2 | import './globals.css' 3 | import { DatalayerProvider } from '@/components/DatalayerProvider' 4 | 5 | const inter = Inter({ subsets: ['latin'] }) 6 | 7 | export default function RootLayout({ 8 | children, 9 | }: { 10 | children: React.ReactNode 11 | }) { 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /examples/spacex/app/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | min-height: 100vh; 6 | } 7 | -------------------------------------------------------------------------------- /examples/spacex/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import styles from './page.module.css' 6 | import Link from 'next/link' 7 | 8 | export default function Page() { 9 | return ( 10 |
    11 |

    Welcome to Fuse

    12 | Try the Streaming SSR example 13 | Try the RSC example 14 |
    15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /examples/spacex/app/rsc/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | min-height: 100vh; 6 | } 7 | 8 | .list { 9 | padding: 0; 10 | margin: 0; 11 | } 12 | -------------------------------------------------------------------------------- /examples/spacex/app/rsc/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { graphql } from '@/fuse' 4 | import { execute } from '@/fuse/server' 5 | import { LaunchItem } from '@/components/LaunchItem' 6 | import { headers } from 'next/headers' 7 | 8 | import styles from './page.module.css' 9 | import { PageNumbers } from '@/components/PageNumbers' 10 | import { LaunchDetails } from '@/components/LaunchDetails' 11 | 12 | const LaunchesQuery = graphql(` 13 | query Launches_RSC($limit: Int, $offset: Int) { 14 | launches(limit: $limit, offset: $offset) { 15 | nodes { 16 | id 17 | ...LaunchFields 18 | } 19 | ...TotalCountFields 20 | } 21 | } 22 | `) 23 | 24 | export default async function Page({ 25 | searchParams, 26 | }: { 27 | searchParams: { offset: string; selected?: string } 28 | }) { 29 | const selectedLaunch = searchParams.selected 30 | const offset = Number(searchParams.offset || 0) 31 | 32 | const result = await execute({ 33 | query: LaunchesQuery, 34 | variables: { limit: 10, offset }, 35 | context: () => ({ user: true }), 36 | }) 37 | 38 | return ( 39 |
    40 |

    SpaceX Launches

    41 |
      42 | {result.data?.launches.nodes.map( 43 | (node) => node && , 44 | )} 45 |
    46 | {result.data && ( 47 | 52 | )} 53 | {selectedLaunch && ( 54 | Loading launch...

    }> 55 | 56 |
    57 | )} 58 |
    59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /examples/spacex/components/DatalayerProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Provider, createClient } from '@/fuse/client' 4 | import React, { Suspense } from 'react' 5 | 6 | export const DatalayerProvider = (props: any) => { 7 | const [client, ssr] = React.useMemo(() => { 8 | const { client, ssr } = createClient({ 9 | url: 10 | process.env.NODE_ENV === 'production' 11 | ? 'https://spacex-fuse.vercel.app/api/fuse' 12 | : 'http://localhost:3000/api/fuse', 13 | }) 14 | 15 | return [client, ssr] 16 | }, []) 17 | 18 | return ( 19 | 20 | {props.children} 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /examples/spacex/components/LaunchDetails.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { graphql } from '@/fuse' 4 | import { useQuery } from '@/fuse/client' 5 | import { LaunchSite } from './LaunchSite' 6 | import { usePathname, useRouter } from 'next/navigation' 7 | 8 | const LaunchDetailsQuery = graphql(` 9 | query LaunchDetails($id: ID!) { 10 | node(id: $id) { 11 | ... on Launch { 12 | id 13 | name 14 | details 15 | launchDate 16 | image 17 | site { 18 | ...LaunchSiteFields 19 | } 20 | rocket { 21 | cost 22 | country 23 | company 24 | description 25 | } 26 | } 27 | } 28 | } 29 | `) 30 | 31 | export const LaunchDetails = (props: { id: string }) => { 32 | const [result] = useQuery({ 33 | query: LaunchDetailsQuery, 34 | variables: { id: props.id }, 35 | }) 36 | 37 | const router = useRouter() 38 | const pathname = usePathname() 39 | 40 | if (result.data?.node?.__typename !== 'Launch') return null 41 | 42 | const { node } = result.data 43 | 44 | return ( 45 | router.replace(`${pathname}`)} 47 | open 48 | style={{ 49 | marginTop: '10%', 50 | padding: 32, 51 | marginLeft: 'auto', 52 | marginRight: 'auto', 53 | }} 54 | > 55 |

    {node.name}

    56 |

    Launched at {new Date(node.launchDate).toUTCString()}

    57 | {node.details &&

    {node.details}

    } 58 | 59 |
    60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /examples/spacex/components/LaunchItem.module.css: -------------------------------------------------------------------------------- 1 | .item { 2 | cursor: pointer; 3 | display: flex; 4 | align-items: center; 5 | padding: 16px; 6 | list-style-type: none; 7 | } 8 | 9 | .badge { 10 | width: 48px; 11 | height: 48px; 12 | margin-right: 8px; 13 | } 14 | 15 | .info { 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | 20 | .launchTitle { 21 | margin: 0; 22 | margin-bottom: 4px; 23 | } 24 | 25 | .launchDate { 26 | margin: 0; 27 | } 28 | -------------------------------------------------------------------------------- /examples/spacex/components/LaunchItem.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { FragmentType, graphql, useFragment } from '@/fuse' 4 | import styles from './LaunchItem.module.css' 5 | import { usePathname, useRouter } from 'next/navigation' 6 | 7 | const LaunchFields = graphql(` 8 | fragment LaunchFields on Launch { 9 | id 10 | name 11 | launchDate 12 | image 13 | } 14 | `) 15 | 16 | export const LaunchItem = (props: { 17 | launch: FragmentType 18 | }) => { 19 | const router = useRouter() 20 | const pathname = usePathname() 21 | const node = useFragment(LaunchFields, props.launch) 22 | 23 | return ( 24 |
  • router.replace(`${pathname}?selected=${node.id}`)} 28 | > 29 | {node.name} 30 | 31 |

    {node.name}

    32 | {node.launchDate && ( 33 |

    34 | Launched at {new Date(node.launchDate).toUTCString()} 35 |

    36 | )} 37 |
    38 |
  • 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /examples/spacex/components/LaunchSite.tsx: -------------------------------------------------------------------------------- 1 | import { FragmentType, graphql, useFragment } from '@/fuse' 2 | import { Location } from './Location' 3 | 4 | const LaunchSiteFields = graphql(` 5 | fragment LaunchSiteFields on Site { 6 | id 7 | name 8 | details 9 | status 10 | location { 11 | ...SiteLocationFields 12 | } 13 | } 14 | `) 15 | 16 | // TODO: make pretty 17 | export const LaunchSite = (props: { 18 | site: FragmentType 19 | }) => { 20 | const result = useFragment(LaunchSiteFields, props.site) 21 | 22 | return ( 23 |
    24 |

    {result.name}

    25 |

    Status: {result.status}

    26 |

    {result.details}

    27 | {result.location && } 28 |
    29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /examples/spacex/components/Location.tsx: -------------------------------------------------------------------------------- 1 | import { FragmentType, graphql, useFragment } from '@/fuse' 2 | 3 | const SiteLocationFields = graphql(` 4 | fragment SiteLocationFields on Location { 5 | latitude 6 | longitude 7 | name 8 | region 9 | } 10 | `) 11 | 12 | export const Location = (props: { 13 | location: FragmentType 14 | }) => { 15 | const result = useFragment(SiteLocationFields, props.location) 16 | 17 | return ( 18 |
    19 |

    20 | {result.region} - {result.name} 21 |

    22 |

    23 | Coordinates {result.longitude} {result.latitude} 24 |

    25 |
    26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /examples/spacex/components/PageNumbers.module.css: -------------------------------------------------------------------------------- 1 | .list { 2 | display: flex; 3 | margin: 0; 4 | margin-top: 12px; 5 | list-style: none; 6 | } 7 | 8 | .pageNumber { 9 | background: none; 10 | color: inherit; 11 | border: none; 12 | padding: 0; 13 | font: inherit; 14 | cursor: pointer; 15 | outline: inherit; 16 | padding: 8px; 17 | } 18 | 19 | .active { 20 | text-decoration: underline; 21 | } 22 | -------------------------------------------------------------------------------- /examples/spacex/components/PageNumbers.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useRouter, usePathname } from 'next/navigation' 4 | 5 | import { FragmentType, graphql, useFragment } from '@/fuse' 6 | 7 | import styles from './PageNumbers.module.css' 8 | import { sayHello } from './actions/sayHello' 9 | 10 | const TotalCountFields = graphql(` 11 | fragment TotalCountFields on QueryLaunchesList { 12 | totalCount 13 | } 14 | `) 15 | 16 | export const PageNumbers = (props: { 17 | list: FragmentType 18 | limit: number 19 | offset: number 20 | }) => { 21 | const router = useRouter() 22 | const pathname = usePathname() 23 | 24 | const node = useFragment(TotalCountFields, props.list) 25 | 26 | if (!node.totalCount) return null 27 | 28 | const amountOfPages = Math.ceil(node.totalCount / props.limit) 29 | const currentPage = props.offset / props.limit 30 | 31 | const sayHelloFuse = sayHello.bind(undefined, { name: 'fuse' }) 32 | 33 | return ( 34 | <> 35 |
      36 | {Array(amountOfPages) 37 | .fill(0) 38 | .map((_, i) => ( 39 |
    • 40 | 50 |
    • 51 | ))} 52 |
    53 |
    54 | 55 |
    56 | 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /examples/spacex/components/Rocket.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StellateHQ/fuse/26ea459f0075fa083734033c96014d37b762089a/examples/spacex/components/Rocket.tsx -------------------------------------------------------------------------------- /examples/spacex/components/actions/sayHello.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { graphql } from '@/fuse' 4 | import { execute } from '@/fuse/server' 5 | import { redirect } from 'next/navigation' 6 | 7 | const SayHello = graphql(` 8 | mutation Hello($name: String!) { 9 | sayHello(name: $name) 10 | } 11 | `) 12 | 13 | export async function sayHello(args: { name: string }) { 14 | const result = await execute({ 15 | query: SayHello, 16 | variables: { name: args.name || 'fuse' }, 17 | }) 18 | 19 | console.log(result.data?.sayHello) 20 | 21 | redirect('/') 22 | } 23 | -------------------------------------------------------------------------------- /examples/spacex/fuse/client.ts: -------------------------------------------------------------------------------- 1 | // This is a generated file! 2 | 3 | export * from 'fuse/next/client' 4 | -------------------------------------------------------------------------------- /examples/spacex/fuse/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fragment-masking' 2 | export * from './gql' 3 | -------------------------------------------------------------------------------- /examples/spacex/fuse/pages.ts: -------------------------------------------------------------------------------- 1 | // This is a generated file! 2 | 3 | export * from 'fuse/next/pages' 4 | -------------------------------------------------------------------------------- /examples/spacex/fuse/server.ts: -------------------------------------------------------------------------------- 1 | // This is a generated file! 2 | 3 | export * from 'fuse/next/server' 4 | export { __internal_execute as execute } from 'fuse/next/server' 5 | -------------------------------------------------------------------------------- /examples/spacex/next.config.js: -------------------------------------------------------------------------------- 1 | const { nextFusePlugin } = require('fuse/next/plugin') 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = nextFusePlugin()({}) 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /examples/spacex/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fuse-examples/spacex", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "typecheck": "tsc" 11 | }, 12 | "dependencies": { 13 | "@graphql-typed-document-node/core": "^3.2.0", 14 | "graphql": "^16.8.1", 15 | "next": "14.0.3", 16 | "react": "^18", 17 | "react-dom": "^18" 18 | }, 19 | "devDependencies": { 20 | "@0no-co/graphqlsp": "^1.0.2", 21 | "@types/node": "^20", 22 | "@types/react": "^18", 23 | "@types/react-dom": "^18", 24 | "@types/webpack-env": "^1.18.4", 25 | "fuse": "workspace:*", 26 | "typescript": "^5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/spacex/pages/test.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { useRouter } from 'next/router' 5 | import { 6 | useQuery, 7 | withGraphQLClient, 8 | initGraphQLClient, 9 | ssrExchange, 10 | cacheExchange, 11 | fetchExchange, 12 | } from '@/fuse/pages' 13 | 14 | import { graphql } from '@/fuse' 15 | 16 | import styles from '../app/client/page.module.css' 17 | 18 | function Page() { 19 | return ( 20 |
    21 |

    SpaceX Launches

    22 | 23 |
    24 | ) 25 | } 26 | 27 | const LaunchesQuery = graphql(` 28 | query PageLaunches($limit: Int, $offset: Int) { 29 | launches(limit: $limit, offset: $offset) { 30 | nodes { 31 | id 32 | name 33 | } 34 | totalCount 35 | } 36 | } 37 | `) 38 | 39 | function Launches() { 40 | const router = useRouter() 41 | 42 | const offset = router.query['offset'] ? Number(router.query['offset']) : 0 43 | 44 | const [result] = useQuery({ 45 | query: LaunchesQuery, 46 | variables: { limit: 10, offset }, 47 | }) 48 | 49 | return ( 50 | <> 51 |
      52 | {result.data?.launches.nodes.map( 53 | (node) => node &&
    • {node.name}
    • , 54 | )} 55 |
    56 | {result.data?.launches.totalCount} 57 | 58 | ) 59 | } 60 | 61 | export async function getServerSideProps() { 62 | const ssrCache = ssrExchange({ isClient: false }) 63 | const client = initGraphQLClient({ 64 | url: 65 | process.env.NODE_ENV === 'production' 66 | ? 'https://spacex-fuse.vercel.app/api/fuse' 67 | : 'http://localhost:3000/api/fuse', 68 | exchanges: [cacheExchange, ssrCache, fetchExchange], 69 | }) 70 | 71 | await client.query(LaunchesQuery, { limit: 10, offset: 0 }).toPromise() 72 | 73 | const graphqlState = ssrCache.extractData() 74 | 75 | return { 76 | props: { 77 | graphqlState, 78 | }, 79 | } 80 | } 81 | 82 | export default withGraphQLClient(() => ({ 83 | url: 84 | process.env.NODE_ENV === 'production' 85 | ? 'https://spacex-fuse.vercel.app/api/fuse' 86 | : 'http://localhost:3000/api/fuse', 87 | }))(Page) 88 | -------------------------------------------------------------------------------- /examples/spacex/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/spacex/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/spacex/schema.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | A date string, such as 2007-12-03, compliant with the `full-date` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. 3 | """ 4 | scalar Date 5 | 6 | """ 7 | The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). 8 | """ 9 | scalar JSON 10 | 11 | type Launch implements Node { 12 | details: String 13 | id: ID! 14 | image: String! 15 | launchDate: String! 16 | name: String! 17 | rocket: Rocket! 18 | site: Site! 19 | } 20 | 21 | type Location { 22 | coordinates: [Float!] 23 | latitude: Float 24 | longitude: Float 25 | name: String 26 | region: String 27 | } 28 | 29 | type Mutation { 30 | _version: String! 31 | sayHello(name: String): String 32 | } 33 | 34 | interface Node { 35 | id: ID! 36 | } 37 | 38 | type Query { 39 | _version: String! 40 | launch(id: ID!): Launch 41 | launches(limit: Int, offset: Int): QueryLaunchesList! 42 | node(id: ID!): Node 43 | nodes(ids: [ID!]!): [Node]! 44 | rocket(id: ID!): Rocket 45 | site(id: ID!): Site 46 | } 47 | 48 | type QueryLaunchesList { 49 | nodes: [Launch]! 50 | totalCount: Int 51 | } 52 | 53 | type Rocket implements Node { 54 | company: String 55 | cost: Int 56 | country: String 57 | description: String 58 | id: ID! 59 | } 60 | 61 | type Site implements Node { 62 | details: String 63 | id: ID! 64 | location: Location 65 | name: String 66 | status: SiteStatus 67 | } 68 | 69 | enum SiteStatus { 70 | ACTIVE 71 | INACTIVE 72 | UNKNOWN 73 | } 74 | -------------------------------------------------------------------------------- /examples/spacex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "Bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "types": [ 17 | "webpack-env" // here 18 | ], 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | }, 23 | { 24 | "name": "@0no-co/graphqlsp", 25 | "schema": "./schema.graphql", 26 | "disableTypegen": true, 27 | "templateIsCallExpression": true, 28 | "template": "graphql" 29 | } 30 | ], 31 | "paths": { 32 | "@/*": ["./*"] 33 | } 34 | }, 35 | "include": [ 36 | "next-env.d.ts", 37 | "**/*.ts", 38 | "**/*.tsx", 39 | ".next/types/**/*.ts", 40 | "types/scopes.ts" 41 | ], 42 | "exclude": ["node_modules"] 43 | } 44 | -------------------------------------------------------------------------------- /examples/spacex/types/launch/Rocket.ts: -------------------------------------------------------------------------------- 1 | import { node, addNodeFields } from 'fuse' 2 | import { LaunchNode } from '../Launch' 3 | 4 | interface Rocket { 5 | id: string 6 | cost_per_launch: number 7 | country: string 8 | company: string 9 | description: string 10 | } 11 | 12 | const RocketNode = node({ 13 | name: 'Rocket', 14 | async load(ids) { 15 | const rockets = await Promise.allSettled( 16 | ids.map((id) => 17 | fetch('https://api.spacexdata.com/v3/rockets/' + id, { 18 | method: 'GET', 19 | }).then((x) => x.json()), 20 | ), 21 | ) 22 | 23 | return await Promise.all( 24 | rockets.map((rocket) => 25 | rocket.status === 'fulfilled' ? rocket.value : new Error(rocket.reason), 26 | ), 27 | ) 28 | }, 29 | fields: (t) => ({ 30 | cost: t.exposeInt('cost_per_launch'), 31 | country: t.exposeString('country'), 32 | company: t.exposeString('company'), 33 | description: t.exposeString('description'), 34 | }), 35 | }) 36 | 37 | addNodeFields(LaunchNode, (t) => ({ 38 | rocket: t.field({ 39 | type: RocketNode, 40 | nullable: false, 41 | resolve: (parent) => parent.rocket.rocket_id, 42 | }), 43 | })) 44 | -------------------------------------------------------------------------------- /examples/spacex/types/scopes.ts: -------------------------------------------------------------------------------- 1 | import 'fuse' 2 | import { defineAuthScopes, Scopes } from 'fuse' 3 | 4 | declare module 'fuse' { 5 | export interface Scopes { 6 | isLoggedIn: boolean 7 | } 8 | } 9 | 10 | defineAuthScopes((ctx) => ({ isLoggedIn: !!ctx.user })) 11 | -------------------------------------------------------------------------------- /examples/standalone/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | build 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /examples/standalone/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json'], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | } 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /examples/standalone/_context.ts: -------------------------------------------------------------------------------- 1 | import type { GetContext, InitialContext } from 'fuse' 2 | 3 | declare module 'fuse' { 4 | export interface UserContext { 5 | ua: string | null 6 | } 7 | } 8 | 9 | export const getContext = ( 10 | ctx: InitialContext, 11 | ): GetContext<{ ua: string | null }> => { 12 | return { 13 | ua: ctx.request.headers.get('user-agent'), 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/standalone/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
    11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/standalone/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fuse-examples/standalone", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "pnpm concurrently 'fuse dev' 'vite --force'", 8 | "build": "vite build && fuse build" 9 | }, 10 | "dependencies": { 11 | "gql.tada": "1.0.1", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0" 14 | }, 15 | "devDependencies": { 16 | "@0no-co/graphqlsp": "^1.0.2", 17 | "@graphql-typed-document-node/core": "^3.2.0", 18 | "@types/react": "^18.2.37", 19 | "@types/react-dom": "^18.2.15", 20 | "@vitejs/plugin-react": "^4.2.0", 21 | "concurrently": "^8.2.2", 22 | "fuse": "file:../../packages/core", 23 | "typescript": "^5.2.2", 24 | "vite": "^5.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/standalone/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/standalone/schema.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | A date string, such as 2007-12-03, compliant with the `full-date` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. 3 | """ 4 | scalar Date 5 | 6 | """ 7 | The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). 8 | """ 9 | scalar JSON 10 | 11 | type Launch implements Node { 12 | details: String 13 | id: ID! 14 | image: String! 15 | launchDate: String! 16 | name: String! 17 | rocket: Rocket! 18 | site: Site! 19 | } 20 | 21 | type Location { 22 | coordinates: [Float!] 23 | latitude: Float 24 | longitude: Float 25 | name: String 26 | region: String 27 | } 28 | 29 | type Mutation { 30 | _version: String! 31 | sayHello(name: String): String 32 | } 33 | 34 | interface Node { 35 | id: ID! 36 | } 37 | 38 | type Query { 39 | _version: String! 40 | launch(id: ID!): Launch 41 | launches(limit: Int, offset: Int): QueryLaunchesList! 42 | node(id: ID!): Node 43 | nodes(ids: [ID!]!): [Node]! 44 | rocket(id: ID!): Rocket 45 | site(id: ID!): Site 46 | } 47 | 48 | type QueryLaunchesList { 49 | nodes: [Launch]! 50 | totalCount: Int 51 | } 52 | 53 | type Rocket implements Node { 54 | company: String 55 | cost: Int 56 | country: String 57 | description: String 58 | id: ID! 59 | } 60 | 61 | type Site implements Node { 62 | details: String 63 | id: ID! 64 | location: Location 65 | name: String 66 | status: SiteStatus 67 | } 68 | 69 | enum SiteStatus { 70 | ACTIVE 71 | INACTIVE 72 | UNKNOWN 73 | } 74 | -------------------------------------------------------------------------------- /examples/standalone/src/components/LaunchDetails.tsx: -------------------------------------------------------------------------------- 1 | import { graphql, useQuery } from '../fuse' 2 | import { LaunchSite, LaunchSiteFields } from './LaunchSite' 3 | 4 | const LaunchDetailsQuery = graphql( 5 | ` 6 | query LaunchDetails($id: ID!) { 7 | node(id: $id) { 8 | __typename 9 | ... on Launch { 10 | id 11 | name 12 | details 13 | launchDate 14 | __typename 15 | site { 16 | ...LaunchSiteFields 17 | } 18 | } 19 | } 20 | } 21 | `, 22 | [LaunchSiteFields], 23 | ) 24 | 25 | export const LaunchDetails = (props: { id: string; deselect: () => void }) => { 26 | const [result] = useQuery({ 27 | query: LaunchDetailsQuery, 28 | variables: { id: props.id }, 29 | }) 30 | 31 | if (result.data?.node?.__typename !== 'Launch') return null 32 | 33 | const { node } = result.data 34 | 35 | return ( 36 | 46 |

    {node.name}

    47 |

    Launched at {new Date(node.launchDate).toUTCString()}

    48 | {node.details &&

    {node.details}

    } 49 | 50 |
    51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /examples/standalone/src/components/LaunchItem.module.css: -------------------------------------------------------------------------------- 1 | .item { 2 | cursor: pointer; 3 | display: flex; 4 | align-items: center; 5 | padding: 16px; 6 | list-style-type: none; 7 | } 8 | 9 | .badge { 10 | width: 48px; 11 | height: 48px; 12 | margin-right: 8px; 13 | } 14 | 15 | .info { 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | 20 | .launchTitle { 21 | margin: 0; 22 | margin-bottom: 4px; 23 | } 24 | 25 | .launchDate { 26 | margin: 0; 27 | } 28 | -------------------------------------------------------------------------------- /examples/standalone/src/components/LaunchItem.tsx: -------------------------------------------------------------------------------- 1 | import { FragmentOf, graphql, readFragment } from '../fuse' 2 | import styles from './LaunchItem.module.css' 3 | 4 | export const LaunchFields = graphql(` 5 | fragment LaunchFields on Launch { 6 | name 7 | launchDate 8 | image 9 | } 10 | `) 11 | 12 | export const LaunchItem = (props: { 13 | launch: FragmentOf 14 | select: () => void 15 | }) => { 16 | const node = readFragment(LaunchFields, props.launch) 17 | return ( 18 |
  • 19 | {node.name} 20 | 21 |

    {node.name}

    22 | {node.launchDate && ( 23 |

    24 | Launched at {new Date(node.launchDate).toUTCString()} 25 |

    26 | )} 27 |
    28 |
  • 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /examples/standalone/src/components/LaunchSite.tsx: -------------------------------------------------------------------------------- 1 | import { FragmentOf, graphql, readFragment } from '../fuse' 2 | import { Location, SiteLocationFields } from './Location' 3 | 4 | export const LaunchSiteFields = graphql( 5 | ` 6 | fragment LaunchSiteFields on Site { 7 | id 8 | name 9 | details 10 | status 11 | location { 12 | ...SiteLocationFields 13 | } 14 | } 15 | `, 16 | [SiteLocationFields], 17 | ) 18 | 19 | export const LaunchSite = (props: { 20 | site: FragmentOf 21 | }) => { 22 | const result = readFragment(LaunchSiteFields, props.site) 23 | 24 | return ( 25 |
    26 |

    {result.name}

    27 |

    Status: {result.status}

    28 |

    {result.details}

    29 | {result.location && } 30 |
    31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /examples/standalone/src/components/Location.tsx: -------------------------------------------------------------------------------- 1 | import { FragmentOf, graphql, readFragment } from '../fuse' 2 | 3 | export const SiteLocationFields = graphql(` 4 | fragment SiteLocationFields on Location { 5 | latitude 6 | longitude 7 | name 8 | region 9 | } 10 | `) 11 | 12 | export const Location = (props: { 13 | location: FragmentOf 14 | }) => { 15 | const result = readFragment(SiteLocationFields, props.location) 16 | 17 | return ( 18 |
    19 |

    20 | {result.region} - {result.name} 21 |

    22 |

    23 | Coordinates {result.longitude} {result.latitude} 24 |

    25 |
    26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /examples/standalone/src/components/PageNumbers.module.css: -------------------------------------------------------------------------------- 1 | .list { 2 | display: flex; 3 | margin: 0; 4 | margin-top: 12px; 5 | list-style: none; 6 | } 7 | 8 | .pageNumber { 9 | background: none; 10 | color: inherit; 11 | border: none; 12 | padding: 0; 13 | font: inherit; 14 | cursor: pointer; 15 | outline: inherit; 16 | padding: 8px; 17 | } 18 | 19 | .active { 20 | text-decoration: underline; 21 | } 22 | -------------------------------------------------------------------------------- /examples/standalone/src/components/PageNumbers.tsx: -------------------------------------------------------------------------------- 1 | import { FragmentType, graphql, useFragment } from '../fuse' 2 | 3 | import styles from './PageNumbers.module.css' 4 | 5 | export const TotalCountFields = graphql(` 6 | fragment TotalCountFields on QueryLaunchesList { 7 | totalCount 8 | } 9 | `) 10 | 11 | export const PageNumbers = (props: { 12 | list: FragmentType 13 | limit: number 14 | offset: number 15 | setOffset: (x: number) => void 16 | }) => { 17 | const node = useFragment(TotalCountFields, props.list) 18 | 19 | if (!node.totalCount) return null 20 | 21 | const amountOfPages = Math.ceil(node.totalCount / props.limit) 22 | const currentPage = props.offset / props.limit 23 | 24 | return ( 25 |
      26 | {Array(amountOfPages) 27 | .fill(0) 28 | .map((_, i) => ( 29 |
    • 30 | 38 |
    • 39 | ))} 40 |
    41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /examples/standalone/src/fuse/index.ts: -------------------------------------------------------------------------------- 1 | // This is a generated file! 2 | 3 | export * from './tada' 4 | export * from 'fuse/client' 5 | -------------------------------------------------------------------------------- /examples/standalone/src/fuse/tada.ts: -------------------------------------------------------------------------------- 1 | import { initGraphQLTada } from 'gql.tada' 2 | import type { introspection } from './introspection' 3 | 4 | export const graphql = initGraphQLTada<{ 5 | introspection: typeof introspection 6 | }>() 7 | 8 | export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada' 9 | export type { FragmentOf as FragmentType } from 'gql.tada' 10 | export { readFragment } from 'gql.tada' 11 | export { readFragment as useFragment } from 'gql.tada' 12 | -------------------------------------------------------------------------------- /examples/standalone/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | width: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | width: 100%; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/standalone/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import { createClient, Provider } from './fuse' 4 | import App from './App.tsx' 5 | import './index.css' 6 | 7 | const client = createClient({ 8 | url: 'http://localhost:4000/graphql', 9 | }) 10 | 11 | ReactDOM.createRoot(document.getElementById('root')!).render( 12 | 13 | 14 | 15 | 16 | , 17 | ) 18 | -------------------------------------------------------------------------------- /examples/standalone/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/standalone/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | "plugins": [ 17 | { 18 | "name": "@0no-co/graphqlsp", 19 | "schema": "./schema.graphql", 20 | "tadaOutputLocation": "./src/fuse/introspection.ts" 21 | } 22 | ], 23 | "paths": { 24 | "@/*": ["./*"] 25 | }, 26 | 27 | /* Linting */ 28 | "strict": true, 29 | "noUnusedLocals": true, 30 | "noUnusedParameters": true, 31 | "noFallthroughCasesInSwitch": true 32 | }, 33 | "include": ["src", "types", "_context.ts"], 34 | "references": [{ "path": "./tsconfig.node.json" }] 35 | } 36 | -------------------------------------------------------------------------------- /examples/standalone/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/standalone/types/launch/Rocket.ts: -------------------------------------------------------------------------------- 1 | import { node, addNodeFields } from 'fuse' 2 | import { LaunchNode } from '../Launch' 3 | 4 | interface Rocket { 5 | id: string 6 | cost_per_launch: number 7 | country: string 8 | company: string 9 | description: string 10 | } 11 | 12 | const RocketNode = node({ 13 | name: 'Rocket', 14 | async load(ids) { 15 | const rockets = await Promise.allSettled( 16 | ids.map((id) => 17 | fetch('https://api.spacexdata.com/v3/rockets/' + id, { 18 | method: 'GET', 19 | }).then((x) => x.json()), 20 | ), 21 | ) 22 | 23 | return await Promise.all( 24 | rockets.map((rocket) => 25 | rocket.status === 'fulfilled' ? rocket.value : new Error(rocket.reason), 26 | ), 27 | ) 28 | }, 29 | fields: (t) => ({ 30 | cost: t.exposeInt('cost_per_launch'), 31 | country: t.exposeString('country'), 32 | company: t.exposeString('company'), 33 | description: t.exposeString('description'), 34 | }), 35 | }) 36 | 37 | addNodeFields(LaunchNode, (t) => ({ 38 | rocket: t.field({ 39 | type: RocketNode, 40 | nullable: false, 41 | resolve: (parent) => parent.rocket.rocket_id, 42 | }), 43 | })) 44 | -------------------------------------------------------------------------------- /examples/standalone/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fuse", 3 | "private": true, 4 | "scripts": { 5 | "build": "pnpm --filter fuse build && pnpm --filter create-fuse-app build", 6 | "dev": "pnpm --filter @fuse-examples/spacex dev", 7 | "prepare": "husky install && pnpm build", 8 | "website": "pnpm --filter @fuse/website dev" 9 | }, 10 | "devDependencies": { 11 | "@changesets/cli": "^2.27.0", 12 | "husky": "^8.0.0", 13 | "lint-staged": "^15.1.0", 14 | "prettier": "^3.1.0", 15 | "typescript": "^5.2.2" 16 | }, 17 | "prettier": { 18 | "tabWidth": 2, 19 | "trailingComma": "all", 20 | "singleQuote": true, 21 | "jsxSingleQuote": true, 22 | "semi": false, 23 | "printWidth": 80 24 | }, 25 | "lint-staged": { 26 | "*.{mjs,js,jsx,ts,tsx,json,md,graphql}": "prettier --write" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # Fuse 2 | 3 | ![Fuse: End-to-end typesafe data fetching for frontend teams at scale](https://images.ctfassets.net/yq1dddfl2vc7/6EDzUh3emBY3uQqoxulmPA/c738d8fbae3e412e38cadee598f3e9db/twitter_header.png) 4 | 5 | # Getting Started 6 | 7 | When you are in the root of your app run the following command. This will 8 | install all the packages and generate the files you need. 9 | 10 | ```sh 11 | npx create-fuse-app 12 | ``` 13 | 14 | Then, run `npx fuse dev` and your API will be running at `localhost:4000/graphql`! 15 | 16 | > If you are **using Next.js, you don't need to manually run `fuse dev`**. `create-fuse-app` will add a Next.js plugin to your `next.config.js/ts/mjs`` and an API route at `/api/fuse` for you to access your API. ([learn more](https://fusedata.dev/docs/setting-fuse-up-manually/nextjs)) 17 | 18 | ## Querying your data layer 19 | 20 | ```tsx 21 | import { graphql } from '@/fuse' 22 | import { execute } from '@/fuse/server' 23 | 24 | const UserQuery = graphql(` 25 | query User($id: ID!) { 26 | user(id: $id) { 27 | id 28 | name 29 | } 30 | } 31 | `) 32 | 33 | export default async function Page() { 34 | const result = await execute({ 35 | query: UserQuery, 36 | variables: { id: '1' }, 37 | }) 38 | 39 | return

    Welcome {result.data?.user?.name}

    40 | } 41 | ``` 42 | 43 | # [Docs](https://fusedata.dev/docs) 44 | 45 | **Read [the documentation](https://fusedata.dev/docs) for more information about using Fuse**. 46 | 47 | Quicklinks to some of the most-visited pages: 48 | 49 | - [Getting started](https://fusedata.dev/docs) 50 | - [Querying your API (client)](https://fusedata.dev/docs/client) 51 | - [Building your API (server)](https://fusedata.dev/docs/server/queries-and-mutations) 52 | - [Deploying your API (server)](https://fusedata.dev/docs/deployment) 53 | - [The Fuse Method](https://fusedata.dev/docs/fuse-method) 54 | 55 | # License 56 | 57 | Licensed under the MIT License, Copyright © 2023-present Stellate, Inc. 58 | 59 | See LICENSE for more information. 60 | -------------------------------------------------------------------------------- /packages/core/client.d.ts: -------------------------------------------------------------------------------- 1 | // src/next/client.ts 2 | import { 3 | useQuery, 4 | UrqlProvider, 5 | ClientOptions, 6 | SSRExchange, 7 | Client, 8 | } from '@urql/next' 9 | export * from 'urql' 10 | export { UrqlProvider as Provider, useQuery } 11 | 12 | type Optional = Pick, K> & Omit 13 | export function createClient(opts: Optional): { 14 | client: Client 15 | ssr: SSRExchange 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/loader.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = function (code) { 4 | const { fs, resourcePath, rootContext: cwd } = this 5 | 6 | if ( 7 | !resourcePath.includes('fuse/route.ts') && 8 | !resourcePath.includes('fuse/server.ts') && 9 | !resourcePath.includes('api/fuse.ts') 10 | ) 11 | return code 12 | 13 | if (code.includes('require.context(')) { 14 | console.warn( 15 | 'Found require.context in code, this can be removed from the codebase.', 16 | ) 17 | return code 18 | } 19 | 20 | let hasSrcDir = false 21 | try { 22 | fs.statSync(path.resolve(cwd, 'src')) 23 | hasSrcDir = true 24 | } catch (e) {} 25 | 26 | let hasTypesDir = false 27 | const typesDir = hasSrcDir 28 | ? path.resolve(cwd, 'src', 'types') 29 | : path.resolve(cwd, 'types') 30 | try { 31 | fs.statSync(typesDir) 32 | hasTypesDir = true 33 | } catch (e) {} 34 | 35 | if (!hasTypesDir) { 36 | return code 37 | } 38 | 39 | function getFiles(dir, files = []) { 40 | const fileList = fs.readdirSync(dir) 41 | for (const file of fileList) { 42 | const name = `${dir}/${file}` 43 | if (fs.statSync(name).isDirectory()) { 44 | getFiles(name, files) 45 | } else { 46 | files.push(name) 47 | } 48 | } 49 | return files 50 | } 51 | 52 | const results = getFiles(typesDir) 53 | 54 | code = `${results.reduce((acc, cur) => { 55 | const curPath = path.resolve(typesDir, cur) 56 | let rel = path.relative(this.context, curPath) 57 | 58 | if (rel === cur) { 59 | rel = `./${rel}` 60 | } 61 | 62 | return `${acc}import '${rel}'\n` 63 | }, '')}\n${code}` 64 | 65 | return code 66 | } 67 | -------------------------------------------------------------------------------- /packages/core/rsc.d.ts: -------------------------------------------------------------------------------- 1 | // src/next/rsc.ts 2 | import { 3 | Client, 4 | ClientOptions, 5 | AnyVariables, 6 | GraphQLRequestParams, 7 | } from '@urql/core' 8 | import { ExecutionResult } from 'graphql' 9 | import { GraphQLParams } from 'graphql-yoga' 10 | import { UserContext } from 'fuse' 11 | 12 | export { registerUrql as registerClient } from '@urql/next/rsc' 13 | export * from '@urql/core' 14 | 15 | type Optional = Pick, K> & Omit 16 | export function createClient(opts: Optional): Client 17 | 18 | export function __internal_execute< 19 | Data = any, 20 | Variables extends AnyVariables = AnyVariables, 21 | >( 22 | request: GraphQLRequestParams & { 23 | context?: (params: GraphQLParams) => UserContext 24 | }, 25 | ): Promise> 26 | -------------------------------------------------------------------------------- /packages/core/src/adapters/bun.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { createYoga } from 'graphql-yoga' 3 | // @ts-ignore 4 | import { builder } from 'fuse' 5 | import { getYogaPlugins, wrappedContext } from '../utils/yoga-helpers' 6 | 7 | export async function main() { 8 | let ctx 9 | import.meta.glob('/types/**/*.ts', { eager: true }) 10 | const context = import.meta.glob('/_context.ts', { eager: true }) 11 | if (context['/_context.ts']) { 12 | const mod = context['/_context.ts'] 13 | if ((mod as any).getContext) { 14 | ctx = (mod as any).getContext 15 | } 16 | } 17 | 18 | const completedSchema = builder.toSchema({}) 19 | 20 | const yoga = createYoga({ 21 | graphiql: false, 22 | maskedErrors: true, 23 | schema: completedSchema, 24 | // We allow batching by default 25 | batching: true, 26 | context: wrappedContext(ctx), 27 | plugins: getYogaPlugins(), 28 | }) 29 | 30 | Bun.serve( 31 | // @ts-ignore this is a typing bug, it works. https://github.com/dotansimha/graphql-yoga/issues/3003 32 | yoga, 33 | ) 34 | } 35 | 36 | main() 37 | -------------------------------------------------------------------------------- /packages/core/src/adapters/cloudflare.ts: -------------------------------------------------------------------------------- 1 | import { createYoga } from 'graphql-yoga' 2 | // @ts-ignore 3 | import { builder } from 'fuse' 4 | import { getYogaPlugins, wrappedContext } from '../utils/yoga-helpers' 5 | 6 | function fetch(request) { 7 | let ctx 8 | import.meta.glob('/types/**/*.ts', { eager: true }) 9 | const context = import.meta.glob('/_context.ts', { eager: true }) 10 | if (context['/_context.ts']) { 11 | const mod = context['/_context.ts'] 12 | if ((mod as any).getContext) { 13 | ctx = (mod as any).getContext 14 | } 15 | } 16 | 17 | const completedSchema = builder.toSchema({}) 18 | 19 | const yoga = createYoga({ 20 | graphiql: false, 21 | maskedErrors: true, 22 | schema: completedSchema, 23 | // We allow batching by default 24 | batching: true, 25 | context: wrappedContext(ctx), 26 | plugins: getYogaPlugins(), 27 | }) 28 | 29 | return yoga.fetch(request, ctx) 30 | } 31 | 32 | export default { fetch } 33 | -------------------------------------------------------------------------------- /packages/core/src/adapters/lambda.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | APIGatewayEvent, 3 | APIGatewayProxyResult, 4 | Context, 5 | } from 'aws-lambda' 6 | import { createYoga } from 'graphql-yoga' 7 | // @ts-ignore 8 | import { builder } from 'fuse' 9 | import { getYogaPlugins, wrappedContext } from '../utils/yoga-helpers' 10 | 11 | export async function fetch( 12 | event: APIGatewayEvent, 13 | lambdaContext: Context, 14 | ): Promise { 15 | let ctx 16 | import.meta.glob('/types/**/*.ts', { eager: true }) 17 | const context = import.meta.glob('/_context.ts', { eager: true }) 18 | if (context['/_context.ts']) { 19 | const mod = context['/_context.ts'] 20 | if ((mod as any).getContext) { 21 | ctx = (mod as any).getContext 22 | } 23 | } 24 | 25 | const completedSchema = builder.toSchema({}) 26 | 27 | const yoga = createYoga({ 28 | graphiql: false, 29 | maskedErrors: true, 30 | schema: completedSchema, 31 | // We allow batching by default 32 | batching: true, 33 | context: wrappedContext(ctx), 34 | plugins: getYogaPlugins(), 35 | }) 36 | 37 | const response = await yoga.fetch( 38 | event.path + 39 | '?' + 40 | new URLSearchParams( 41 | (event.queryStringParameters as Record) || {}, 42 | ).toString(), 43 | { 44 | method: event.httpMethod, 45 | headers: event.headers as HeadersInit, 46 | body: event.body 47 | ? Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8') 48 | : undefined, 49 | }, 50 | { 51 | event, 52 | lambdaContext, 53 | }, 54 | ) 55 | 56 | const responseHeaders = Object.fromEntries(response.headers.entries()) 57 | 58 | return { 59 | statusCode: response.status, 60 | headers: responseHeaders, 61 | body: await response.text(), 62 | isBase64Encoded: false, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/core/src/adapters/node.ts: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import { createYoga } from 'graphql-yoga' 3 | // @ts-ignore 4 | import { builder } from 'fuse' 5 | import { getYogaPlugins, wrappedContext } from '../utils/yoga-helpers' 6 | 7 | export async function main() { 8 | let ctx 9 | import.meta.glob('/types/**/*.ts', { eager: true }) 10 | const context = import.meta.glob('/_context.ts', { eager: true }) 11 | if (context['/_context.ts']) { 12 | const mod = context['/_context.ts'] 13 | if ((mod as any).getContext) { 14 | ctx = (mod as any).getContext 15 | } 16 | } 17 | 18 | const completedSchema = builder.toSchema({}) 19 | 20 | const yoga = createYoga({ 21 | graphiql: false, 22 | maskedErrors: true, 23 | schema: completedSchema, 24 | // We allow batching by default 25 | batching: true, 26 | context: wrappedContext(ctx), 27 | plugins: getYogaPlugins(), 28 | }) 29 | 30 | const server = http.createServer(yoga) 31 | server.listen(process.env.PORT || 4000) 32 | } 33 | 34 | main() 35 | -------------------------------------------------------------------------------- /packages/core/src/client.ts: -------------------------------------------------------------------------------- 1 | import { createClient as create, fetchExchange } from 'urql' 2 | import type { Client, ClientOptions } from 'urql' 3 | import { cacheExchange } from './exchanges/cache' 4 | 5 | export * from 'urql' 6 | 7 | export { cacheExchange } from './exchanges/cache' 8 | 9 | type Optional = Pick, K> & Omit 10 | export const createClient = ( 11 | opts: Optional, 12 | ): Client => { 13 | const options: ClientOptions = { 14 | ...opts, 15 | suspense: opts.suspense ?? true, 16 | exchanges: opts.exchanges || [cacheExchange, fetchExchange], 17 | } 18 | return create(options) 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/dev.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { builder } from 'fuse' 3 | import { printSchema } from 'graphql' 4 | import { createYoga } from 'graphql-yoga' 5 | 6 | import { getYogaPlugins, wrappedContext } from './utils/yoga-helpers' 7 | 8 | // prettier-ignore 9 | const defaultQuery = /* GraphQL */ `query { 10 | _version 11 | } 12 | ` 13 | 14 | export async function main() { 15 | const modules = import.meta.glob('/types/**/*.ts') 16 | const context = import.meta.glob('/_context.ts') 17 | 18 | const promises: Array = [] 19 | let ctx 20 | if (context['/_context.ts']) { 21 | promises.push( 22 | context['/_context.ts']().then((mod) => { 23 | if ((mod as any).getContext) { 24 | ctx = (mod as any).getContext 25 | } 26 | }), 27 | ) 28 | } 29 | 30 | for (const path in modules) { 31 | promises.push(modules[path]()) 32 | } 33 | 34 | await Promise.all(promises) 35 | 36 | const completedSchema = builder.toSchema({}) 37 | 38 | const yoga = createYoga({ 39 | schema: completedSchema, 40 | // We allow batching by default 41 | graphiql: { 42 | title: 'Fuse GraphiQL', 43 | defaultQuery, 44 | }, 45 | batching: true, 46 | context: wrappedContext(ctx), 47 | plugins: getYogaPlugins(), 48 | }) 49 | 50 | ;(yoga as any).stringifiedSchema = printSchema(completedSchema) 51 | return yoga 52 | } 53 | -------------------------------------------------------------------------------- /packages/core/src/errors.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql' 2 | 3 | export abstract class FuseError extends GraphQLError { 4 | abstract readonly name: string 5 | 6 | constructor( 7 | message: string, 8 | extensions: { 9 | code: string 10 | http?: { 11 | status: number 12 | } 13 | }, 14 | ) { 15 | super(message, { 16 | extensions, 17 | }) 18 | } 19 | } 20 | 21 | /** For use when user is not authenticated or unknown. */ 22 | export class AuthenticationError extends FuseError { 23 | name = 'UnauthenticatedError' 24 | constructor(message = 'Unauthenticated') { 25 | super(message, { code: 'UNAUTHENTICATED' }) 26 | } 27 | } 28 | 29 | /** For use when a resource is not found or not accessible by an authenticated user. */ 30 | export class ForbiddenError extends FuseError { 31 | name = 'ForbiddenError' 32 | constructor(message = 'Forbidden') { 33 | super(message, { code: 'FORBIDDEN' }) 34 | } 35 | } 36 | 37 | /** For use when a resource is not found. */ 38 | export class NotFoundError extends FuseError { 39 | name = 'NotFoundError' 40 | constructor(message = 'Not Found') { 41 | super(message, { code: 'NOT_FOUND' }) 42 | } 43 | } 44 | 45 | /** For use when any input was invalid or when a resource does not exist but is assumed to exist. */ 46 | export class BadRequestError extends FuseError { 47 | name = 'BadRequestError' 48 | constructor(message = 'Bad Request') { 49 | super(message, { code: 'BAD_REQUEST' }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/core/src/next/client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useQuery, 3 | UrqlProvider, 4 | createClient as create, 5 | fetchExchange, 6 | ssrExchange, 7 | } from '@urql/next' 8 | import type { Client, ClientOptions, SSRExchange } from '@urql/next' 9 | import { cacheExchange } from '../exchanges/cache' 10 | 11 | export * from 'urql' 12 | export { useQuery, UrqlProvider as Provider } 13 | export { cacheExchange } from '../exchanges/cache' 14 | 15 | type Optional = Pick, K> & Omit 16 | export const createClient = ( 17 | opts: Optional, 18 | ): { client: Client; ssr: SSRExchange } => { 19 | const ssr = ssrExchange() 20 | const options: ClientOptions = { 21 | ...opts, 22 | suspense: opts.suspense ?? true, 23 | exchanges: opts.exchanges || [cacheExchange, ssr, fetchExchange], 24 | } 25 | return { client: create(options), ssr } 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/pothos-list/index.ts: -------------------------------------------------------------------------------- 1 | import './global-types' 2 | import './schema-builder' 3 | import SchemaBuilder, { 4 | BasePlugin, 5 | FieldKind, 6 | SchemaTypes, 7 | RootFieldBuilder, 8 | } from '@pothos/core' 9 | import { ListShape } from './types' 10 | 11 | const pluginName = 'fuselist' as const 12 | 13 | export default pluginName 14 | 15 | export class PothosListPlugin< 16 | Types extends SchemaTypes, 17 | > extends BasePlugin {} 18 | 19 | try { 20 | SchemaBuilder.registerPlugin(pluginName, PothosListPlugin) 21 | } catch (e) {} 22 | 23 | const fieldBuilderProto = 24 | RootFieldBuilder.prototype as PothosSchemaTypes.RootFieldBuilder< 25 | SchemaTypes, 26 | unknown, 27 | FieldKind 28 | > 29 | 30 | fieldBuilderProto.list = function list(fieldOptions) { 31 | const ref = 32 | this.builder.objectRef>( 33 | 'Unnamed list', 34 | ) 35 | 36 | const fieldRef = this.field({ 37 | ...fieldOptions, 38 | type: ref, 39 | args: { 40 | ...fieldOptions.args, 41 | }, 42 | resolve: fieldOptions.resolve as never, 43 | } as never) 44 | 45 | this.builder.configStore.onFieldUse(fieldRef, (fieldConfig) => { 46 | const name = fieldConfig.name[0].toUpperCase() + fieldConfig.name.slice(1) 47 | const listName = `${this.typename}${name}${ 48 | fieldConfig.name.toLowerCase().endsWith('list') ? '' : 'List' 49 | }` 50 | 51 | this.builder.listObject({ 52 | type: fieldOptions.type, 53 | name: listName, 54 | nullable: fieldOptions.nodeNullable ?? true, 55 | }) 56 | 57 | this.builder.configStore.associateRefWithName(ref, listName) 58 | }) 59 | 60 | return fieldRef 61 | } 62 | -------------------------------------------------------------------------------- /packages/core/src/pothos-list/schema-builder.ts: -------------------------------------------------------------------------------- 1 | import SchemaBuilder, { ObjectRef, SchemaTypes, verifyRef } from '@pothos/core' 2 | import { ListShape } from './types' 3 | 4 | const schemaBuilderProto = 5 | SchemaBuilder.prototype as PothosSchemaTypes.SchemaBuilder 6 | 7 | export const listRefs = new WeakMap< 8 | PothosSchemaTypes.SchemaBuilder, 9 | ObjectRef>[] 10 | >() 11 | 12 | export const globalListFieldsMap = new WeakMap< 13 | PothosSchemaTypes.SchemaBuilder, 14 | ((ref: ObjectRef>) => void)[] 15 | >() 16 | 17 | schemaBuilderProto.listObject = function listObject({ 18 | type, 19 | name: listName, 20 | nullable, 21 | }) { 22 | verifyRef(type) 23 | 24 | const listRef = 25 | this.objectRef>(listName) 26 | 27 | this.objectType(listRef, { 28 | fields: (t) => ({ 29 | totalCount: t.int({ 30 | nullable: true, 31 | resolve: (parent) => parent.totalCount || null, 32 | }), 33 | nodes: t.field({ 34 | nullable: { 35 | items: nullable ?? true, 36 | list: false, 37 | }, 38 | type: [type], 39 | resolve: (parent) => parent.nodes as any, 40 | }), 41 | }), 42 | }) 43 | 44 | if (!listRefs.has(this)) { 45 | listRefs.set(this, []) 46 | } 47 | 48 | listRefs.get(this)!.push(listRef) 49 | 50 | globalListFieldsMap.get(this)?.forEach((fieldFn) => void fieldFn(listRef)) 51 | 52 | return listRef as never 53 | } 54 | -------------------------------------------------------------------------------- /packages/core/src/pothos-list/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SchemaTypes, 3 | MaybePromise, 4 | ShapeFromTypeParam, 5 | OutputType, 6 | } from '@pothos/core' 7 | 8 | export interface ListResultShape { 9 | totalCount?: number | null 10 | nodes: MaybePromise[] 11 | } 12 | 13 | export type ListShape< 14 | Types extends SchemaTypes, 15 | T, 16 | Nullable, 17 | ListResult extends ListResultShape = ListResultShape, 18 | > = 19 | | (Nullable extends false ? never : null | undefined) 20 | | (ListResult & Types['ListWrapper']) 21 | 22 | export type ListShapeFromBaseShape< 23 | Types extends SchemaTypes, 24 | Shape, 25 | Nullable extends boolean, 26 | > = ListShape 27 | 28 | export type ListShapeForType< 29 | Types extends SchemaTypes, 30 | Type extends OutputType, 31 | Nullable extends boolean, 32 | ListResult extends ListResultShape< 33 | ShapeFromTypeParam 34 | > = ListResultShape>, 35 | > = ListShape< 36 | Types, 37 | ShapeFromTypeParam, 38 | Nullable, 39 | ListResult 40 | > 41 | 42 | export type ListShapeFromResolve< 43 | Types extends SchemaTypes, 44 | Type extends OutputType, 45 | Nullable extends boolean, 46 | Resolved, 47 | ListResult extends ListResultShape< 48 | ShapeFromTypeParam 49 | > = ListResultShape>, 50 | > = Resolved extends Promise 51 | ? NonNullable extends ListShapeForType 52 | ? NonNullable 53 | : ListShapeForType & NonNullable 54 | : Resolved extends ListShapeForType 55 | ? NonNullable 56 | : ListShapeForType & 57 | NonNullable 58 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fuse-fixtures/simple", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "devDependencies": { 7 | "fuse": "file:../../../", 8 | "typescript": "^5.2.2" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/simple/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/simple/types/Test.ts: -------------------------------------------------------------------------------- 1 | import { node } from 'fuse' 2 | 3 | type UserSource = { 4 | id: string 5 | name: string 6 | avatar_url: string 7 | } 8 | 9 | // "Nodes" are the core abstraction of Fuse. Each node represents 10 | // a resource/entity with multiple fields and has to define two things: 11 | // 1. load(): How to fetch from the underlying data source 12 | // 2. fields: What fields should be exposed and added for clients 13 | export const UserNode = node({ 14 | name: 'User', 15 | load: async (ids) => getUsers(ids), 16 | fields: (t) => ({ 17 | name: t.exposeString('name'), 18 | // rename to camel-case 19 | avatarUrl: t.exposeString('avatar_url'), 20 | // Add an additional firstName field 21 | firstName: t.string({ 22 | resolve: (user) => user.name.split(' ')[0], 23 | }), 24 | }), 25 | }) 26 | 27 | // Fake function to fetch users. In real applications, this would 28 | // talk to an underlying REST API/gRPC service/third-party API/… 29 | async function getUsers(ids: string[]): Promise { 30 | return ids.map((id) => ({ 31 | id, 32 | name: `Peter #${id}`, 33 | avatar_url: `https://i.pravatar.cc/300?u=${id}`, 34 | })) 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/tada/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fuse-fixtures/tada", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "devDependencies": { 7 | "@0no-co/graphqlsp": "1.3.3", 8 | "fuse": "file:../../../", 9 | "typescript": "^5.2.2", 10 | "gql.tada": "1.2.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/tada/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "plugins": [ 9 | { 10 | "name": "@0no-co/graphqlsp", 11 | "schema": "./schema.graphql", 12 | "tadaOutputLocation": "./fuse/introspection.ts" 13 | } 14 | ], 15 | "moduleResolution": "bundler", 16 | "allowImportingTsExtensions": true, 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/tada/types/Test.ts: -------------------------------------------------------------------------------- 1 | import { node } from 'fuse' 2 | 3 | type UserSource = { 4 | id: string 5 | name: string 6 | avatar_url: string 7 | } 8 | 9 | // "Nodes" are the core abstraction of Fuse. Each node represents 10 | // a resource/entity with multiple fields and has to define two things: 11 | // 1. load(): How to fetch from the underlying data source 12 | // 2. fields: What fields should be exposed and added for clients 13 | export const UserNode = node({ 14 | name: 'User', 15 | load: async (ids) => getUsers(ids), 16 | fields: (t) => ({ 17 | name: t.exposeString('name'), 18 | // rename to camel-case 19 | avatarUrl: t.exposeString('avatar_url'), 20 | // Add an additional firstName field 21 | firstName: t.string({ 22 | resolve: (user) => user.name.split(' ')[0], 23 | }), 24 | }), 25 | }) 26 | 27 | // Fake function to fetch users. In real applications, this would 28 | // talk to an underlying REST API/gRPC service/third-party API/… 29 | async function getUsers(ids: string[]): Promise { 30 | return ids.map((id) => ({ 31 | id, 32 | name: `Peter #${id}`, 33 | avatar_url: `https://i.pravatar.cc/300?u=${id}`, 34 | })) 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "lib": ["esnext"], 5 | "jsx": "preserve", 6 | "target": "es2018", 7 | "module": "es2020", 8 | "moduleResolution": "node", 9 | "allowJs": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "declaration": false, 14 | "noEmit": true, 15 | "strict": true, 16 | "noImplicitAny": false, 17 | "noUnusedParameters": false, 18 | "forceConsistentCasingInFileNames": true, 19 | "isolatedModules": true, 20 | "useUnknownInCatchVariables": false, 21 | "types": ["node", "vite/client"] 22 | }, 23 | "exclude": ["**/dist", "**/build"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | 5 | test: { 6 | alias: { 7 | fuse: './builder.mjs' 8 | } 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /packages/create-fuse-app/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # create-fuse-app 2 | 3 | ## 0.7.0 4 | 5 | ### Minor Changes 6 | 7 | - db8b67d: Support creating a fuse app in an empty directory 8 | 9 | ## 0.6.0 10 | 11 | ### Minor Changes 12 | 13 | - 1f225c5: Fix typo in the babel-plugin when rewriting an export default expression 14 | 15 | ## 0.5.0 16 | 17 | ### Minor Changes 18 | 19 | - d55a2f0: Default to using `gql.tada` 20 | 21 | ## 0.4.1 22 | 23 | ### Patch Changes 24 | 25 | - 23c0264: Fix writing of `.mjs` next config 26 | 27 | ## 0.4.0 28 | 29 | ### Minor Changes 30 | 31 | - 2b4073e: Add support for generating a fuse-app without being in Next.JS 32 | 33 | ### Patch Changes 34 | 35 | - 0a58c27: Check for `src` when looking for the `/app` directory 36 | 37 | ## 0.3.0 38 | 39 | ### Minor Changes 40 | 41 | - e7f037b: Add a webpack-loader that automatically imports all entries in the `types/` directory. 42 | In doing so it removes the need for `require.context`, next time you run the application, 43 | you are encouraged to remove `require.context` from your `/pages/api/fuse.ts` or `/app/api/fuse/route.ts` 44 | files. 45 | 46 | ## 0.2.1 47 | 48 | ### Patch Changes 49 | 50 | - f699e31: Fix duplicate entries 51 | 52 | ## 0.2.0 53 | 54 | ### Minor Changes 55 | 56 | - fd89e23: Add support for `bun` and `pnpm` 57 | 58 | ## 0.1.1 59 | 60 | ### Patch Changes 61 | 62 | - 35abb16: Fix crash when dir exists 63 | -------------------------------------------------------------------------------- /packages/create-fuse-app/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Stellate 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. -------------------------------------------------------------------------------- /packages/create-fuse-app/README.md: -------------------------------------------------------------------------------- 1 | # create-fuse-app 2 | 3 | This will automatically generate all the needed files to get 4 | started with [`fuse`](https://fusedata.dev/) 5 | 6 | ```sh 7 | npx create-fuse-app 8 | ## or 9 | npm init fuse-app 10 | ## or 11 | yarn create fuse-app 12 | ## or 13 | pnpm create fuse-app 14 | ``` 15 | 16 | ## [Read the docs](https://fusedata.dev/docs): [fusedata.dev/docs](https://fusedata.dev) 17 | -------------------------------------------------------------------------------- /packages/create-fuse-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-fuse-app", 3 | "version": "0.7.0", 4 | "description": "The magical GraphQL framework", 5 | "homepage": "https://github.com/StellateHQ/fuse", 6 | "bugs": "https://github.com/StellateHQ/fuse/issues", 7 | "license": "MIT", 8 | "author": "Stellate engineering ", 9 | "keywords": [], 10 | "bin": "./dist/index.js", 11 | "type": "module", 12 | "files": [ 13 | "dist", 14 | "LICENSE", 15 | "README.md", 16 | "CHANGELOG.md" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/StellateHQ/fuse", 21 | "directory": "packages/create-fuse-app" 22 | }, 23 | "scripts": { 24 | "build": "tsup", 25 | "prepublishOnly": "tsup" 26 | }, 27 | "dependencies": { 28 | "@babel/core": "^7.23.5", 29 | "@clack/prompts": "^0.7.0", 30 | "comment-json": "^4.2.3", 31 | "execa": "^8.0.1", 32 | "kolorist": "^1.8.0" 33 | }, 34 | "devDependencies": { 35 | "@types/node": "^20.10.3", 36 | "tsup": "^7.2.0", 37 | "type-fest": "^4.8.3", 38 | "typescript": "^5.3.2" 39 | }, 40 | "publishConfig": { 41 | "access": "public", 42 | "provenance": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/create-fuse-app/src/get-package-manager.ts: -------------------------------------------------------------------------------- 1 | // This is copied from https://github.com/vercel/next.js/blob/canary/packages/create-next-app/helpers/get-pkg-manager.ts 2 | export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' 3 | 4 | export function getPkgManager(): PackageManager { 5 | const userAgent = process.env.npm_config_user_agent || '' 6 | 7 | if (userAgent.startsWith('yarn')) { 8 | return 'yarn' 9 | } 10 | 11 | if (userAgent.startsWith('pnpm')) { 12 | return 'pnpm' 13 | } 14 | 15 | if (userAgent.startsWith('bun')) { 16 | return 'bun' 17 | } 18 | 19 | return 'npm' 20 | } 21 | -------------------------------------------------------------------------------- /packages/create-fuse-app/src/install-package.ts: -------------------------------------------------------------------------------- 1 | import type { PackageManager } from './get-package-manager' 2 | import { execa } from 'execa' 3 | 4 | export async function install( 5 | packageManager: PackageManager, 6 | env: 'prod' | 'dev', 7 | packages: string[], 8 | ): Promise { 9 | let args: string[] = [] 10 | switch (packageManager) { 11 | case 'npm': { 12 | args.push('install') 13 | if (env === 'dev') { 14 | args.push('--save-dev') 15 | } else { 16 | args.push('--save') 17 | } 18 | break 19 | } 20 | case 'yarn': { 21 | args.push('add') 22 | if (env === 'dev') { 23 | args.push('-D') 24 | } 25 | break 26 | } 27 | case 'pnpm': { 28 | args.push('add') 29 | if (env === 'dev') { 30 | args.push('-D') 31 | } 32 | break 33 | } 34 | case 'bun': { 35 | args.push('add') 36 | if (env === 'dev') { 37 | args.push('-D') 38 | } 39 | break 40 | } 41 | } 42 | 43 | args.push(...packages) 44 | /** 45 | * Return a Promise that resolves once the installation is finished. 46 | */ 47 | await execa(packageManager, args, { 48 | stdio: 'inherit', 49 | env: { 50 | ...process.env, 51 | NODE_ENV: 'development', 52 | }, 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /packages/create-fuse-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "lib": ["esnext"], 5 | "jsx": "preserve", 6 | "target": "es2018", 7 | "module": "es2020", 8 | "moduleResolution": "node", 9 | "allowJs": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "declaration": false, 14 | "noEmit": true, 15 | "strict": true, 16 | "noImplicitAny": false, 17 | "noUnusedParameters": false, 18 | "forceConsistentCasingInFileNames": true, 19 | "isolatedModules": true, 20 | "useUnknownInCatchVariables": false, 21 | "types": ["node"] 22 | }, 23 | "exclude": ["**/dist", "**/build"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/create-fuse-app/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, Options } from 'tsup' 2 | 3 | export default defineConfig(async () => { 4 | const baseOptions: Options = { 5 | platform: 'node', 6 | 7 | splitting: false, 8 | format: ['esm'], 9 | skipNodeModulesBundle: false, 10 | target: 'node18', 11 | env: { 12 | // env var `npm_package_version` gets injected in runtime by npm/yarn automatically 13 | // this replacement is for build time, so it can be used for both 14 | npm_package_version: 15 | process.env.npm_package_version ?? 16 | (await import('./package.json')).version, 17 | }, 18 | minify: false, 19 | clean: true, 20 | } 21 | 22 | /** 23 | * We create distinct options so that no type declarations are reused and 24 | * exported into a separate file, in other words, we want all `d.ts` files 25 | * to not contain any imports. 26 | */ 27 | return [ 28 | { 29 | ...baseOptions, 30 | entry: ['src/index.ts'], 31 | }, 32 | ] 33 | }) 34 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'examples/*' 3 | - 'packages/core' 4 | - 'packages/core/test/fixtures/*' 5 | - 'packages/create-fuse-app' 6 | - 'website' 7 | -------------------------------------------------------------------------------- /website/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | -------------------------------------------------------------------------------- /website/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | public 4 | pnpm-lock.yaml -------------------------------------------------------------------------------- /website/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "plugins": ["prettier-plugin-tailwindcss"] 5 | } 6 | -------------------------------------------------------------------------------- /website/.svgrrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | typescript: true, 3 | outDir: './src/components/icons/', 4 | prettier: false, 5 | expandProps: 'end', 6 | svgProps: { 7 | 'aria-hidden': 'true', 8 | }, 9 | svgoConfig: { 10 | plugins: [ 11 | { 12 | name: 'preset-default', 13 | params: { 14 | overrides: { 15 | removeViewBox: false, 16 | }, 17 | }, 18 | }, 19 | ], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | ## Fuse website 2 | 3 | Build using Nextra. 4 | 5 | ### Development 6 | 7 | ```bash 8 | # run from monorepo root 9 | pnpm website 10 | ``` 11 | 12 | ### Udpating sitemap 13 | 14 | When adding new pages, run `pnpm --filter @fuse/website build` to update the sitemap files. 15 | -------------------------------------------------------------------------------- /website/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /website/next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next-sitemap').IConfig} */ 2 | module.exports = { 3 | siteUrl: 'https://fusedata.dev', 4 | generateRobotsTxt: true, 5 | } 6 | -------------------------------------------------------------------------------- /website/next.config.js: -------------------------------------------------------------------------------- 1 | const withNextra = require('nextra')({ 2 | theme: 'nextra-theme-docs', 3 | themeConfig: './theme.config.tsx', 4 | }) 5 | 6 | /** @type {import('next').NextConfig} */ 7 | const nextConfig = { 8 | reactStrictMode: true, 9 | transpilePackages: ['geist', 'react-tweet'], 10 | } 11 | 12 | module.exports = withNextra(nextConfig) 13 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fuse/website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build && next-sitemap", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "generate": "svgr -- src/components/icons/svg && prettier ./src/components/icons --write" 11 | }, 12 | "dependencies": { 13 | "@radix-ui/react-tooltip": "^1.0.7", 14 | "@vercel/analytics": "^1.1.1", 15 | "clsx": "^2.0.0", 16 | "geist": "^1.1.0", 17 | "next": "14.0.3", 18 | "next-sitemap": "^4.2.3", 19 | "nextra": "^2.13.2", 20 | "nextra-theme-docs": "^2.13.2", 21 | "react": "^18.0.0", 22 | "react-countup": "^6.5.0", 23 | "react-dom": "^18.0.0", 24 | "react-fast-marquee": "^1.6.2", 25 | "react-tweet": "^3.2.0", 26 | "sass": "^1.69.5", 27 | "tailwind-merge": "^2.0.0" 28 | }, 29 | "devDependencies": { 30 | "@svgr/cli": "^8.1.0", 31 | "@svgr/webpack": "^8.1.0", 32 | "@types/node": "^20", 33 | "@types/react": "^18", 34 | "@types/react-dom": "^18", 35 | "autoprefixer": "^10.0.1", 36 | "eslint": "^8", 37 | "eslint-config-next": "14.0.3", 38 | "postcss": "^8", 39 | "prettier": "3.1.0", 40 | "prettier-plugin-tailwindcss": "0.5.7", 41 | "tailwindcss": "^3.3.0", 42 | "typescript": "^5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /website/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StellateHQ/fuse/26ea459f0075fa083734033c96014d37b762089a/website/public/favicon.ico -------------------------------------------------------------------------------- /website/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StellateHQ/fuse/26ea459f0075fa083734033c96014d37b762089a/website/public/favicon.png -------------------------------------------------------------------------------- /website/public/images/data-layers-vs-api-gateways.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StellateHQ/fuse/26ea459f0075fa083734033c96014d37b762089a/website/public/images/data-layers-vs-api-gateways.png -------------------------------------------------------------------------------- /website/public/images/data-layers-vs-bffs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StellateHQ/fuse/26ea459f0075fa083734033c96014d37b762089a/website/public/images/data-layers-vs-bffs.png -------------------------------------------------------------------------------- /website/public/images/data-layers-vs-graphql-federation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StellateHQ/fuse/26ea459f0075fa083734033c96014d37b762089a/website/public/images/data-layers-vs-graphql-federation.png -------------------------------------------------------------------------------- /website/public/images/fuse-circles-with-logos.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StellateHQ/fuse/26ea459f0075fa083734033c96014d37b762089a/website/public/images/fuse-circles-with-logos.webp -------------------------------------------------------------------------------- /website/public/images/fuse-grid-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StellateHQ/fuse/26ea459f0075fa083734033c96014d37b762089a/website/public/images/fuse-grid-logo.webp -------------------------------------------------------------------------------- /website/public/images/fuse-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/public/images/nextjs-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /website/public/images/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StellateHQ/fuse/26ea459f0075fa083734033c96014d37b762089a/website/public/images/og-image.png -------------------------------------------------------------------------------- /website/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/public/robots.txt: -------------------------------------------------------------------------------- 1 | # * 2 | User-agent: * 3 | Allow: / 4 | 5 | # Host 6 | Host: https://fusedata.dev 7 | 8 | # Sitemaps 9 | Sitemap: https://fusedata.dev/sitemap.xml 10 | -------------------------------------------------------------------------------- /website/public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://fusejs.org/sitemap-0.xml 4 | -------------------------------------------------------------------------------- /website/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/public/videos/video-poster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StellateHQ/fuse/26ea459f0075fa083734033c96014d37b762089a/website/public/videos/video-poster.png -------------------------------------------------------------------------------- /website/public/videos/video-sample-vertical.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StellateHQ/fuse/26ea459f0075fa083734033c96014d37b762089a/website/public/videos/video-sample-vertical.mp4 -------------------------------------------------------------------------------- /website/public/videos/video-sample.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StellateHQ/fuse/26ea459f0075fa083734033c96014d37b762089a/website/public/videos/video-sample.mp4 -------------------------------------------------------------------------------- /website/public/videos/video-vertical-poster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StellateHQ/fuse/26ea459f0075fa083734033c96014d37b762089a/website/public/videos/video-vertical-poster.png -------------------------------------------------------------------------------- /website/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react' 2 | import { cn } from '@/utils/tailwind' 3 | import { ClassValue } from 'clsx' 4 | 5 | type ButtonVariant = 'light' | 'dark' | 'starship' 6 | 7 | function getClassNames(variant: ButtonVariant, className?: ClassValue) { 8 | return cn( 9 | 'inline-flex items-center gap-2 rounded-[30px] px-[20px] py-[10px] text-sm font-medium', 10 | variant === 'light' 11 | ? 'border-[0.5px] border-gravel-300 bg-white text-black shadow-actions hover:bg-gravel-100 hover:shadow-light-button-hover-shadow' 12 | : variant === 'dark' 13 | ? 'bg-gravel-950 text-white hover:bg-gravel-800 hover:shadow-dark-button-hover-shadow' 14 | : 'bg-starship-950 text-starship-500 hover:text-starship-950 hover:bg-starship-500 hover:shadow-starship-button-hover-shadow', 15 | className, 16 | ) 17 | } 18 | 19 | type ButtonProps = { 20 | variant: ButtonVariant 21 | } & ComponentProps<'button'> 22 | 23 | export function Button({ className, ...props }: ButtonProps) { 24 | return ( 25 |