1
0
mirror of synced 2026-02-04 03:01:17 -05:00

Compare commits

...

10 Commits

Author SHA1 Message Date
Brandon Bayer
b1ef45bf28 2.0.0-alpha.2 - fix generator npm package dist 2022-04-14 20:44:45 -04:00
Brandon Bayer
c6e8df5efd release 2.0.0-alpha.1 2022-04-14 20:31:40 -04:00
Brandon Bayer
46a34c7b3a install changeset 2022-04-14 20:28:16 -04:00
Dillon Raphael
18d4ef74a9 Migrate cli generate command (#3294)
* migrate generate command

* tidy up the help command
2022-04-14 16:49:59 -04:00
Dillon Raphael
212a1cb941 Cli codegen command (#3276)
* WIP: init cli + init env utils

* remove pnpm global link artifacts

* WIP: migrate legacy new cli command

* change enums to objects

* remove chromedriver package from integration test

* create cli bin file that requires blitz/dist/index.cj

* fix prisma-utils pEvent

* fix missing modules

* use form key types & rename bin script

* init cli-codegen

* add fs-extra package to blitz-next

* read config file as a buffer then eval the string

Co-authored-by: Dillon Raphael <dillonraphael@Dillons-MBP.hitronhub.home>
2022-04-14 11:17:19 -04:00
Aleksandra
151dcc308e Add RpcClientPlugin 🎉 (#3302) 2022-04-13 14:24:26 +02:00
Aleksandra
fd90cbedb4 Support blitz next CMD (#3272) 2022-04-13 11:23:49 +02:00
Brandon Bayer
10d27c74af refactor toolkit setup a bit for better DX (#3297) 2022-04-12 15:56:15 -04:00
Brandon Bayer
9810d984f1 toolkit: remove hook exports from setupClient (#3287)
* cleanup exports

* bump

* Update example app

Co-authored-by: Aleksandra <alexsandra.sikora@gmail.com>
2022-04-12 14:31:10 +02:00
Brandon Bayer
10574b7359 Add RPC package to toolkit, including magical imports 🎉 (#3278)
* wip

* webpack loader working

* time to switch to babel

* wip

* wip - next broken

* build & babel working, but resolver not loaded at runtime

* webpack loaders fully working!

* rpc server handler ported

* fully working e2e!

* ci

* fix

* fix

* fix test

* refactor integration test setup

* port rpc integration test

* remove duplicate constants

* fix timeout

* add more explicit timeouts for tests

* use pnpm exec

* add custom _document for auth test

* await killapp

* add next as dependency for test utils

* update pnpm ci

* add vitest config file

* add hookTimeout to global vitest file

Co-authored-by: Dillon Raphael <dillon@creatorsneverdie.com>
2022-04-11 14:18:02 -04:00
120 changed files with 6263 additions and 710 deletions

8
.changeset/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

11
.changeset/config.json Normal file
View File

@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [["blitz"], ["@blitzjs/*"]],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["web", "test-*"]
}

View File

@@ -0,0 +1,9 @@
---
"blitz": patch
"@blitzjs/auth": patch
"@blitzjs/next": patch
"@blitzjs/rpc": patch
"@blitzjs/generator": patch
---
initial publish

View File

@@ -0,0 +1,5 @@
---
"@blitzjs/generator": patch
---
fix generator npm package dist

18
.changeset/pre.json Normal file
View File

@@ -0,0 +1,18 @@
{
"mode": "pre",
"tag": "alpha",
"initialVersions": {
"web": "0.0.0",
"test-auth": "0.0.0",
"test-rpc": "0.0.0",
"test-utils": "0.0.0",
"blitz": "2.0.0-alpha.0",
"@blitzjs/auth": "2.0.0-alpha.0",
"@blitzjs/next": "2.0.0-alpha.0",
"@blitzjs/rpc": "2.0.0-alpha.0",
"@blitzjs/config": "0.0.0",
"@blitzjs/generator": "2.0.0-alpha.0",
"template": "0.0.0"
},
"changesets": ["ninety-pets-heal", "poor-peas-lick"]
}

View File

@@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v2
- uses: pnpm/action-setup@646cdf48217256a3d0b80361c5a50727664284f2
with:
version: 6.10.0
version: 6.32.6
- name: Setup node
uses: actions/setup-node@v2
with:
@@ -32,7 +32,7 @@ jobs:
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm manypkg check
- run: pnpm lint
- run: pnpm build
- run: pnpm lint
- run: pnpm build:apps
- run: pnpm test

View File

@@ -3,4 +3,4 @@
pnpm manypkg check
pnpm lint
pnpx pretty-quick --staged
pnpm pretty-quick --staged

View File

@@ -1,3 +1,19 @@
# Contributing
[Read the Contributing Guide at Blitzjs.com](https://blitzjs.com/docs/contributing)
## To run tests
Make sure you have `chromedriver` installed for your Chrome version. You can install it with
- `brew install --cask chromedriver` on Mac OS X
- `chocolatey install chromedriver` on Windows
- Or manually download the version that matches your installed chrome version (if there's no match, download a version under it, but not above) from the [chromedriver repo](https://chromedriver.storage.googleapis.com/index.html) and add the binary to `<next-repo>/node_modules/.bin`
You may also have to [install Rust](https://www.rust-lang.org/tools/install) and build our native packages to see all tests pass locally. We check in binaries for the most common targets and those required for CI so that most people don't have to, but if you do not see a binary for your target in `packages/next/native`, you can build it by running `yarn --cwd packages/next build-native`. If you are working on the Rust code and you need to build the binaries for ci, you can manually trigger [the workflow](https://github.com/vercel/next.js/actions/workflows/build_native.yml) to build and commit with the "Run workflow" button.
Running all tests:
```sh
pnpm test
```

View File

@@ -0,0 +1,20 @@
import {AuthClientPlugin} from "@blitzjs/auth"
import {setupClient} from "@blitzjs/next"
import {BlitzRpcPlugin} from "@blitzjs/rpc"
const {withBlitz} = setupClient({
plugins: [
AuthClientPlugin({
cookiePrefix: "webapp-cookie-prefix",
}),
BlitzRpcPlugin({
reactQueryOptions: {
queries: {
staleTime: 7000,
},
},
}),
],
})
export {withBlitz}

View File

@@ -0,0 +1,14 @@
import {Ctx} from "blitz"
import {prisma} from "../../prisma"
import {User} from "prisma"
export default async function createUser(
input: {name: string; email: string},
ctx: Ctx,
): Promise<User> {
ctx.session.$authorize()
const user = await prisma.user.create({data: {name: input.name, email: input.email}})
return user
}

View File

@@ -0,0 +1,4 @@
export default function setBasic(input, ctx) {
console.log("SET BASIC input", input)
return
}

View File

@@ -0,0 +1,5 @@
export default async function getBasic(input, ctx) {
console.log("INPUT", input)
return "basic-result"
}

View File

@@ -0,0 +1,11 @@
import {Ctx} from "blitz"
import {prisma} from "../../prisma"
import {User} from "prisma"
export default async function getUsers(_input: {}, ctx: Ctx): Promise<User[]> {
ctx.session.$authorize()
const users = await prisma.user.findMany()
return users
}

View File

@@ -0,0 +1,5 @@
export default function getV2Basic(input, ctx) {
console.log("INPUT", input)
return "basic-result"
}

View File

@@ -1,7 +1,10 @@
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
})
const {withBlitz} = require("@blitzjs/next")
module.exports = withBundleAnalyzer({
reactStrictMode: true,
})
module.exports = withBlitz(
withBundleAnalyzer({
reactStrictMode: true,
}),
)

View File

@@ -16,11 +16,12 @@
"@blitzjs/auth": "workspace:*",
"@blitzjs/config": "workspace:*",
"@blitzjs/next": "workspace:*",
"@blitzjs/rpc": "workspace:*",
"@prisma/client": "3.9.0",
"@types/jest": "27.4.1",
"blitz": "workspace:*",
"jest": "27.5.1",
"next": "12.1.4",
"next": "12.1.1",
"prisma": "3.9.0",
"react": "18.0.0",
"react-dom": "18.0.0",

View File

@@ -1,8 +1,8 @@
import {ErrorFallbackProps, ErrorComponent, ErrorBoundary} from "@blitzjs/next"
import {AuthenticationError, AuthorizationError} from "blitz"
import type {AppProps} from "next/app"
import React from "react"
import {withBlitz} from "../src/client-setup"
import React, {Suspense} from "react"
import {withBlitz} from "app/blitz-client"
function RootErrorFallback({error}: ErrorFallbackProps) {
if (error instanceof AuthenticationError) {
@@ -27,7 +27,9 @@ function RootErrorFallback({error}: ErrorFallbackProps) {
function MyApp({Component, pageProps}: AppProps) {
return (
<ErrorBoundary FallbackComponent={RootErrorFallback}>
<Component {...pageProps} />
<Suspense fallback="Loading...">
<Component {...pageProps} />
</Suspense>
</ErrorBoundary>
)
}

View File

@@ -1,4 +1,4 @@
import {api} from "../../src/server-setup"
import {api} from "app/blitz-server"
import {SessionContext} from "@blitzjs/auth"
import {prisma} from "../../prisma/index"

View File

@@ -1,4 +1,4 @@
import {api} from "../../src/server-setup"
import {api} from "app/blitz-server"
export default api(async (_req, res, ctx) => {
const {session} = ctx

View File

@@ -1,4 +1,4 @@
import {api} from "../../src/server-setup"
import {api} from "app/blitz-server"
export default api(async (_req, res, ctx) => {
const blitzContext = ctx

View File

@@ -1,4 +1,4 @@
import {api} from "../../src/server-setup"
import {api} from "app/blitz-server"
import {prisma} from "../../prisma/index"
export default api(async (_req, res) => {

View File

@@ -0,0 +1,4 @@
import {rpcHandler} from "@blitzjs/rpc"
import {api} from "app/blitz-server"
export default api(rpcHandler({onError: console.log}))

View File

@@ -1,6 +1,5 @@
import {setPublicDataForUser} from "@blitzjs/auth"
import {api} from "../../src/server-setup"
import {api} from "app/blitz-server"
import {prisma} from "../../prisma/index"
export default api(async (req, res, ctx) => {

View File

@@ -1,4 +1,4 @@
import {api} from "../../src/server-setup"
import {api} from "app/blitz-server"
import {prisma} from "../../prisma/index"
import {SecurePassword} from "@blitzjs/auth"

View File

@@ -1,4 +1,4 @@
import {api} from "../../src/server-setup"
import {api} from "app/blitz-server"
import {prisma} from "../../prisma/index"
import {SecurePassword} from "@blitzjs/auth"

View File

@@ -0,0 +1,41 @@
import {useState} from "react"
import {SessionContext} from "@blitzjs/auth"
import {useMutation} from "@blitzjs/rpc"
import createUser from "app/mutations/createUser"
import {User} from "prisma"
function Page() {
const [name, setName] = useState("")
const [email, setEmail] = useState("")
const [createUserMutation, {error}] = useMutation(createUser)
const [newUser, setNewUser] = useState<null | User>(null)
return (
<div>
New User Form:
<form
onSubmit={async (e) => {
e.preventDefault()
const user = await createUserMutation({name, email})
setNewUser(user)
}}
>
<label>
Name:
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
</label>
<label>
Email:
<input type="text" value={email} onChange={(e) => setEmail(e.target.value)} />
</label>
<button type="submit">Create User</button>
</form>
<div style={{paddingTop: 20}}>
<div>Error: {JSON.stringify(error, null, 2)}</div>
New user: {JSON.stringify(newUser, null, 2)}
</div>
</div>
)
}
export default Page

View File

@@ -1,3 +1,6 @@
import {invoke} from "@blitzjs/rpc"
import getBasic from "app/queries/getBasic"
export const getServerSideProps = () => {
return {props: {}}
}
@@ -6,6 +9,7 @@ export default function Web() {
return (
<div>
<h1>Web</h1>
<button onClick={() => invoke(getBasic, "FROM BROWSER")}>GetBasic</button>
</div>
)
}

View File

@@ -1,4 +1,4 @@
const PageWithRedirect = () => {
const PageWithAuthRedirect = () => {
return (
<div>
{JSON.stringify(
@@ -13,6 +13,6 @@ const PageWithRedirect = () => {
)
}
PageWithRedirect.redirectAuthenticatedTo = "/"
PageWithAuthRedirect.redirectAuthenticatedTo = "/"
export default PageWithRedirect
export default PageWithAuthRedirect

View File

@@ -1,4 +1,4 @@
import {gSP} from "../src/server-setup"
import {gSP} from "app/blitz-server"
export const getStaticProps = gSP(async ({ctx}) => {
return {
@@ -14,8 +14,8 @@ export const getStaticProps = gSP(async ({ctx}) => {
}
})
function Page({data}) {
function PageWithGsp({data}) {
return <div>{JSON.stringify(data, null, 2)}</div>
}
export default Page
export default PageWithGsp

View File

@@ -1,5 +1,5 @@
import {SessionContext} from "@blitzjs/auth"
import {gSSP} from "../src/server-setup"
import {gSSP} from "app/blitz-server"
type Props = {
userId: unknown
@@ -16,8 +16,8 @@ export const getServerSideProps = gSSP<Props>(async ({ctx}) => {
}
})
function Page(props: Props) {
function PageWithGssp(props: Props) {
return <div>{JSON.stringify(props, null, 2)}</div>
}
export default Page
export default PageWithGssp

View File

@@ -1,6 +1,6 @@
import {useAuthenticatedSession} from "../src/client-setup"
import {useAuthenticatedSession} from "@blitzjs/auth"
export default function PgaeWithUseAuthorizeIf() {
export default function PageWithUseAuthSession() {
useAuthenticatedSession()
return <div>This page is using useAuthenticatedSession</div>
}

View File

@@ -1,6 +1,6 @@
import {useAuthorizeIf} from "../src/client-setup"
import {useAuthorizeIf} from "@blitzjs/auth"
export default function PgaeWithUseAuthorizeIf() {
export default function PageWithUseAuthorizeIf() {
useAuthorizeIf(Math.random() > 0.5)
return <div>This page is using useAuthorizeIf</div>
}

View File

@@ -1,4 +1,4 @@
import {useAuthorize} from "../src/client-setup"
import {useAuthorize} from "@blitzjs/auth"
export default function PgaeWithUseAuthorize() {
useAuthorize()

View File

@@ -1,6 +1,6 @@
import {useRedirectAuthenticated} from "../src/client-setup"
import {useRedirectAuthenticated} from "@blitzjs/auth"
export default function PgaeWithUseAuthorizeIf() {
export default function PageWithUseRedirectAuth() {
useRedirectAuthenticated("/")
return <div>This page is using useRedirectAuthenticated</div>
}

View File

@@ -1,4 +1,4 @@
import {useSession} from "../src/client-setup"
import {useSession} from "@blitzjs/auth"
export default function PgaeWithUseSession() {
const data = useSession()

View File

@@ -1,7 +1,7 @@
const PageWithRedirect = ({data}) => {
const PageWithoutFlicker = ({data}) => {
return <div>{JSON.stringify(data)}</div>
}
PageWithRedirect.suppressFirstRenderFlicker = true
PageWithoutFlicker.suppressFirstRenderFlicker = true
export default PageWithRedirect
export default PageWithoutFlicker

20
apps/web/pages/users.tsx Normal file
View File

@@ -0,0 +1,20 @@
import {useQuery} from "@blitzjs/rpc"
import getUsers from "app/queries/getUsers"
function UsersPage() {
const [users] = useQuery(getUsers, {})
return (
<div>
Users:
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
)
}
export default UsersPage

View File

@@ -1,26 +0,0 @@
import {AuthClientPlugin} from "@blitzjs/auth"
import {setupClient} from "@blitzjs/next"
const {
withBlitz,
useSession,
useAuthorize,
useAuthorizeIf,
useRedirectAuthenticated,
useAuthenticatedSession,
} = setupClient({
plugins: [
AuthClientPlugin({
cookiePrefix: "webapp-cookie-prefix",
}),
],
})
export {
withBlitz,
useSession,
useAuthorize,
useAuthorizeIf,
useRedirectAuthenticated,
useAuthenticatedSession,
}

View File

@@ -1,5 +1,8 @@
{
"extends": "@blitzjs/config/tsconfig.nextjs.json",
"compilerOptions": {
"baseUrl": "."
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "jest.config.js"],
"exclude": ["node_modules", "test"]
}

View File

@@ -0,0 +1,12 @@
import {AuthClientPlugin} from "@blitzjs/auth"
import {setupClient} from "@blitzjs/next"
const {withBlitz} = setupClient({
plugins: [
AuthClientPlugin({
cookiePrefix: "auth-tests-cookie-prefix",
}),
],
})
export {withBlitz}

View File

@@ -6,7 +6,7 @@ import {prisma as db} from "../prisma/index"
const {gSSP, gSP, api} = setupBlitz({
plugins: [
AuthServerPlugin({
cookiePrefix: "webapp-cookie-prefix",
cookiePrefix: "auth-tests-cookie-prefix",
storage: PrismaStorage(db as any),
isAuthorized: simpleRolesIsAuthorized,
}),

View File

@@ -1,10 +1,4 @@
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
})
module.exports = withBundleAnalyzer({
reactStrictMode: true,
experimental: {
esmExternals: "loose",
},
const {withBlitz} = require("@blitzjs/next")
module.exports = withBlitz({
// update me
})

View File

@@ -19,7 +19,7 @@
"@prisma/client": "3.9.0",
"blitz": "workspace:*",
"lowdb": "3.0.0",
"next": "12.1.4",
"next": "12.1.1",
"prisma": "3.9.0",
"react": "18.0.0",
"react-dom": "18.0.0"
@@ -28,22 +28,13 @@
"@next/bundle-analyzer": "12.0.8",
"@types/express": "4.17.13",
"@types/fs-extra": "9.0.13",
"@types/get-port": "4.2.0",
"@types/node-fetch": "2.6.1",
"@types/react": "17.0.43",
"@types/rimraf": "3.0.2",
"@types/selenium-webdriver": "4.0.18",
"b64-lite": "1.4.0",
"chromedriver": "100.0.0",
"cross-spawn": "7.0.3",
"eslint": "7.32.0",
"express": "4.17.3",
"fs-extra": "10.0.1",
"get-port": "6.1.2",
"node-fetch": "3.2.3",
"rimraf": "3.0.2",
"selenium-webdriver": "4.1.1",
"tree-kill": "1.2.2",
"typescript": "^4.5.3"
}
}

View File

@@ -2,7 +2,7 @@ import {ErrorFallbackProps, ErrorComponent, ErrorBoundary} from "@blitzjs/next"
import {AuthenticationError, AuthorizationError} from "blitz"
import type {AppProps} from "next/app"
import React from "react"
import {withBlitz} from "../src/client-setup"
import {withBlitz} from "../app/blitz-client"
function RootErrorFallback({error}: ErrorFallbackProps) {
if (error instanceof AuthenticationError) {

View File

@@ -1,4 +1,4 @@
import {api} from "../../src/server-setup"
import {api} from "../../app/blitz-server"
export default api(async (_req, res, ctx) => {
const blitzContext = ctx

View File

@@ -1,4 +1,4 @@
import {api} from "../../src/server-setup"
import {api} from "../../app/blitz-server"
export default api(async (_req, res, ctx) => {
res.status(200).end()

View File

@@ -1,4 +1,4 @@
import {api} from "../../src/server-setup"
import {api} from "../../app/blitz-server"
import {prisma} from "../../prisma/index"
import {SecurePassword} from "@blitzjs/auth"

View File

@@ -1,4 +1,4 @@
import {useAuthorize} from "../src/client-setup"
import {useAuthorize} from "@blitzjs/auth"
export const getServerSideProps = () => {
return {props: {}}

View File

@@ -1,26 +0,0 @@
import {AuthClientPlugin} from "@blitzjs/auth"
import {setupClient} from "@blitzjs/next"
const {
withBlitz,
useSession,
useAuthorize,
useAuthorizeIf,
useRedirectAuthenticated,
useAuthenticatedSession,
} = setupClient({
plugins: [
AuthClientPlugin({
cookiePrefix: "webapp-cookie-prefix",
}),
],
})
export {
withBlitz,
useSession,
useAuthorize,
useAuthorizeIf,
useRedirectAuthenticated,
useAuthenticatedSession,
}

View File

@@ -1,6 +1,7 @@
import {describe, it, expect, beforeAll, afterAll} from "vitest"
import {killApp, findPort, launchApp, nextBuild, nextStart} from "./next-test-utils"
import webdriver from "./next-webdriver"
import {killApp, findPort, launchApp, nextBuild, nextStart} from "../../utils/next-test-utils"
import webdriver from "../../utils/next-webdriver"
import {join} from "path"
import seed from "../prisma/seed"
import fetch from "node-fetch"
@@ -10,10 +11,10 @@ let app: any
let appPort: number
const appDir = join(__dirname, "../")
const HEADER_CSRF = "anti-csrf"
const COOKIE_PUBLIC_DATA_TOKEN = "webapp-cookie-prefix_sPublicDataToken"
const COOKIE_SESSION_TOKEN = "webapp-cookie-prefix_sSessionToken"
const COOKIE_ANONYMOUS_SESSION_TOKEN = "webapp-cookie-prefix_sAnonymousSessionToken"
const COOKIE_REFRESH_TOKEN = "webapp-cookie-prefix_sIdRefreshToken"
const COOKIE_PUBLIC_DATA_TOKEN = "auth-tests-cookie-prefix_sPublicDataToken"
const COOKIE_SESSION_TOKEN = "auth-tests-cookie-prefix_sSessionToken"
const COOKIE_ANONYMOUS_SESSION_TOKEN = "auth-tests-cookie-prefix_sAnonymousSessionToken"
const COOKIE_REFRESH_TOKEN = "auth-tests-cookie-prefix_sIdRefreshToken"
const HEADER_PUBLIC_DATA_TOKEN = "public-data-token"
function readCookie(cookieHeader, name) {
@@ -127,7 +128,7 @@ describe("dev mode", () => {
console.log(error)
}
}, 5000 * 60 * 2)
afterAll(() => killApp(app))
afterAll(async () => await killApp(app))
runTests()
})
@@ -141,7 +142,7 @@ describe("server mode", () => {
console.log(err)
}
}, 5000 * 60 * 2)
afterAll(() => killApp(app))
afterAll(async () => await killApp(app))
runTests()
})

View File

@@ -0,0 +1,4 @@
export default async function setBasic(input, ctx) {
global.basic = input
return global.basic
}

View File

@@ -0,0 +1,12 @@
if (typeof window !== "undefined") {
throw new Error("This should not be loaded on the client")
}
export default async function getBasic() {
if (typeof window !== "undefined") {
throw new Error("This should not be loaded on the client")
}
global.basic ??= "basic-result"
return global.basic
}

View File

@@ -0,0 +1,3 @@
export default async function getFailure() {
throw new Error("error on purpose for test")
}

View File

@@ -0,0 +1,3 @@
export default async function getNestedBasic() {
return "nested-basic"
}

5
integration-tests/rpc/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@@ -0,0 +1,4 @@
const {withBlitz} = require("@blitzjs/next")
module.exports = withBlitz({
// update me
})

View File

@@ -0,0 +1,29 @@
{
"name": "test-rpc",
"version": "0.0.0",
"private": true,
"scripts": {
"test": "vitest --config ./vitest.config.ts run",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next"
},
"dependencies": {
"@blitzjs/auth": "workspace:*",
"@blitzjs/config": "workspace:*",
"@blitzjs/next": "workspace:*",
"@blitzjs/rpc": "workspace:*",
"blitz": "workspace:*",
"next": "12.1.1",
"react": "18.0.0",
"react-dom": "18.0.0"
},
"devDependencies": {
"@types/express": "4.17.13",
"@types/fs-extra": "9.0.13",
"@types/node-fetch": "2.6.1",
"@types/react": "17.0.43",
"b64-lite": "1.4.0",
"eslint": "7.32.0",
"fs-extra": "10.0.1",
"typescript": "^4.5.3"
}
}

View File

@@ -0,0 +1,3 @@
import {rpcHandler} from "@blitzjs/rpc"
export default rpcHandler({onError: console.log})

View File

@@ -0,0 +1,4 @@
const Page = () => {
return <div id="page-container">Hello World</div>
}
export default Page

View File

@@ -0,0 +1,7 @@
import getBasic from "../app/queries/getBasic"
const Page = () => {
getBasic().then(console.log)
return <div id="page-container">Hello World</div>
}
export default Page

View File

@@ -0,0 +1,199 @@
import {describe, it, expect, beforeAll, afterAll} from "vitest"
import fs from "fs-extra"
import {join} from "path"
import {
killApp,
findPort,
launchApp,
fetchViaHTTP,
nextBuild,
nextStart,
nextExport,
getPageFileFromBuildManifest,
getPageFileFromPagesManifest,
} from "../../utils/next-test-utils"
// jest.setTimeout(1000 * 60 * 2)
const appDir = join(__dirname, "../")
const nextConfig = join(appDir, "next.config.js")
let appPort
let mode
let app
function runTests(dev = false) {
describe("api requests", () => {
it(
"returns 200 for HEAD",
async () => {
const res = await fetchViaHTTP(appPort, "/api/rpc/getBasic", null, {
method: "HEAD",
})
expect(res.status).toEqual(200)
},
5000 * 60 * 2,
)
it(
"returns 404 for GET",
async () => {
const res = await fetchViaHTTP(appPort, "/api/rpc/getBasic", null, {
method: "GET",
})
expect(res.status).toEqual(404)
},
5000 * 60 * 2,
)
it(
"requires params",
async () => {
const res = await fetchViaHTTP(appPort, "/api/rpc/getBasic", null, {
method: "POST",
headers: {"Content-Type": "application/json; charset=utf-8"},
})
const json = await res.json()
expect(res.status).toEqual(400)
expect(json.error.message).toBe("Request body is missing the `params` key")
},
5000 * 60 * 2,
)
it(
"query works",
async () => {
const data = await fetchViaHTTP(appPort, "/api/rpc/getBasic", null, {
method: "POST",
headers: {"Content-Type": "application/json; charset=utf-8"},
body: JSON.stringify({params: {}}),
}).then((res) => res.ok && res.json())
expect(data).toEqual({result: "basic-result", error: null, meta: {}})
},
5000 * 60 * 2,
)
it(
"mutation works",
async () => {
const data = await fetchViaHTTP(appPort, "/api/rpc/setBasic", null, {
method: "POST",
headers: {"Content-Type": "application/json; charset=utf-8"},
body: JSON.stringify({params: "new-basic"}),
}).then((res) => res.ok && res.json())
expect(data).toEqual({result: "new-basic", error: null, meta: {}})
const data2 = await fetchViaHTTP(appPort, "/api/rpc/getBasic", null, {
method: "POST",
headers: {"Content-Type": "application/json; charset=utf-8"},
body: JSON.stringify({params: {}}),
}).then((res) => res.ok && res.json())
expect(data2).toEqual({result: "new-basic", error: null, meta: {}})
},
5000 * 60 * 2,
)
it(
"handles resolver errors",
async () => {
const res = await fetchViaHTTP(appPort, "/api/rpc/getFailure", null, {
method: "POST",
headers: {"Content-Type": "application/json; charset=utf-8"},
body: JSON.stringify({params: {}}),
})
const json = await res.json()
expect(res.status).toEqual(200)
expect(json).toEqual({
result: null,
error: {name: "Error", message: "error on purpose for test", statusCode: 500},
meta: {error: {values: ["Error"]}},
})
},
5000 * 60 * 2,
)
it(
"nested query works",
async () => {
const data = await fetchViaHTTP(appPort, "/api/rpc/v2/getNestedBasic", null, {
method: "POST",
headers: {"Content-Type": "application/json; charset=utf-8"},
body: JSON.stringify({params: {}}),
}).then((res) => res.ok && res.json())
expect(data).toEqual({result: "nested-basic", error: null, meta: {}})
},
5000 * 60 * 2,
)
})
if (!dev) {
it("should show warning with next export", async () => {
const {stderr} = await nextExport(appDir, {outdir: join(appDir, "out")}, {stderr: true})
expect(stderr).toContain("https://nextjs.org/docs/messages/api-routes-static-export")
})
}
}
describe("RPC", () => {
describe(
"dev mode",
() => {
beforeAll(async () => {
try {
appPort = await findPort()
app = await launchApp(appDir, appPort)
} catch (err) {
console.log(err)
}
})
afterAll(() => killApp(app))
runTests(true)
},
5000 * 60 * 2,
)
describe(
"server mode",
() => {
beforeAll(async () => {
await nextBuild(appDir)
mode = "server"
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(() => killApp(app))
runTests()
},
5000 * 60 * 2,
)
describe(
"serverless mode",
() => {
let nextConfigContent = ""
const nextConfigPath = join(appDir, "next.config.js")
beforeAll(async () => {
nextConfigContent = await fs.readFile(nextConfigPath, "utf8")
await fs.writeFile(
nextConfigPath,
nextConfigContent.replace("// update me", `target: 'experimental-serverless-trace',`),
)
await nextBuild(appDir)
mode = "serverless"
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
await fs.writeFile(nextConfigPath, nextConfigContent)
})
runTests()
},
5000 * 60 * 2,
)
})

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,8 @@
import {defineConfig} from "vitest/config"
export default defineConfig({
test: {
testTimeout: 5000 * 60 * 2,
hookTimeout: 5000 * 60 * 2,
},
})

View File

@@ -96,9 +96,7 @@ interface RunNextCommandOptions {
}
export function runNextCommand(argv: any[], options: RunNextCommandOptions = {}) {
const nextDir = path.dirname(require.resolve("next/package"))
const nextBin = path.join(nextDir, "dist/bin/next")
const cwd = options.cwd || nextDir
const cwd = options.cwd
// Let Next.js decide the environment
const env = {
...process.env,
@@ -109,7 +107,7 @@ export function runNextCommand(argv: any[], options: RunNextCommandOptions = {})
return new Promise<any>((resolve, reject) => {
console.log(`Running command "next ${argv.join(" ")}"`)
const instance = spawn("node", ["--no-deprecation", nextBin, ...argv], {
const instance = spawn("pnpm", ["exec", "next", ...argv], {
...options.spawnOptions,
cwd,
env,
@@ -167,11 +165,8 @@ interface RunNextCommandDevOptions {
nextStart?: boolean
}
export function runNextCommandDev(argv, stdOut, opts: RunNextCommandDevOptions = {}) {
const nextDir = path.dirname(require.resolve("next/package"))
const nextBin = path.join(nextDir, "dist/bin/next")
const cwd = opts.cwd || nextDir
export function runNextCommandDev(argv, opts: RunNextCommandDevOptions = {}) {
const cwd = opts.cwd // || nextDir
const env = {
...process.env,
NODE_ENV: opts.nextStart ? ("production" as const) : ("development" as const),
@@ -179,9 +174,8 @@ export function runNextCommandDev(argv, stdOut, opts: RunNextCommandDevOptions =
...opts.env,
}
const nodeArgs = opts.nodeArgs || []
return new Promise<void | string | ChildProcess>((resolve, reject) => {
const instance = spawn("node", [...nodeArgs, "--no-deprecation", nextBin, ...argv], {
const instance = spawn("pnpm", ["exec", "next", ...argv], {
cwd,
env,
})
@@ -236,7 +230,7 @@ export function runNextCommandDev(argv, stdOut, opts: RunNextCommandDevOptions =
// Launch the app in dev mode.
export function launchApp(dir, port, opts) {
return runNextCommandDev([dir, "-p", port], undefined, opts)
return runNextCommandDev(["-p", port], {cwd: dir, ...opts})
}
export function nextBuild(dir, args = [], opts = {}) {
@@ -244,26 +238,27 @@ export function nextBuild(dir, args = [], opts = {}) {
}
export function nextExport(dir, {outdir}, opts = {}) {
return runNextCommand(["export", dir, "--outdir", outdir], opts)
return runNextCommand(["export", dir, "--outdir", outdir], {cwd: dir, ...opts})
}
export function nextExportDefault(dir, opts = {}) {
return runNextCommand(["export", dir], opts)
return runNextCommand(["export", dir], {cwd: dir, ...opts})
}
export function nextLint(dir, args = [], opts = {}) {
return runNextCommand(["lint", dir, ...args], opts)
return runNextCommand(["lint", dir, ...args], {cwd: dir, ...opts})
}
export function nextStart(dir, port, opts = {}) {
return runNextCommandDev(["start", "-p", port, dir], undefined, {
return runNextCommandDev(["start", "-p", port], {
cwd: dir,
...opts,
nextStart: true,
})
}
export function buildTS(args = [], cwd, env = {NODE_ENV: "production" as const}) {
cwd = cwd || path.dirname(require.resolve("next/package"))
cwd = cwd
env = {...process.env, ...env}
return new Promise<void>((resolve, reject) => {

View File

@@ -144,6 +144,19 @@ const getDeviceIP = async () => {
}
}
export const closeBrowser = async () => {
// First we close all extra windows left over
let allWindows = await browser.getAllWindowHandles()
for (const win of allWindows) {
if (win === initialWindow) continue
try {
await browser.switchTo().window(win)
await browser.close()
} catch (_) {}
}
}
// eslint-disable-next-line no-unused-vars
const freshWindow = async (appPort) => {
// First we close all extra windows left over

View File

@@ -0,0 +1,24 @@
{
"name": "test-utils",
"version": "0.0.0",
"private": true,
"devDependencies": {
"@types/express": "4.17.13",
"@types/fs-extra": "9.0.13",
"@types/node-fetch": "2.6.1",
"@types/react": "17.0.43",
"@types/rimraf": "3.0.2",
"@types/selenium-webdriver": "4.0.18",
"chromedriver": "100.0.0",
"cross-spawn": "7.0.3",
"eslint": "7.32.0",
"express": "4.17.3",
"fs-extra": "10.0.1",
"get-port": "6.1.2",
"node-fetch": "3.2.3",
"rimraf": "3.0.2",
"selenium-webdriver": "4.1.1",
"tree-kill": "1.2.2",
"typescript": "^4.5.3"
}
}

View File

@@ -16,15 +16,17 @@
"lint": "turbo run lint",
"test": "turbo run test",
"clean": "turbo run clean && rm -rf node_modules",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"release": "changeset publish && git push --follow-tags"
},
"dependencies": {
"@blitzjs/manypkg": "0.19.1",
"@changesets/cli": "2.22.0",
"eslint": "7.32.0",
"husky": "7.0.4",
"jsdom": "19.0.0",
"lint-staged": "12.1.7",
"next": "12.1.4",
"next": "12.1.1",
"only-allow": "1.1.0",
"patch-package": "6.4.7",
"prettier": "^2.5.1",

View File

@@ -0,0 +1,15 @@
# @blitzjs/auth
## 2.0.0-alpha.2
### Patch Changes
- blitz@2.0.0-alpha.2
## 2.0.0-alpha.1
### Patch Changes
- 46a34c7b: initial publish
- Updated dependencies [46a34c7b]
- blitz@2.0.0-alpha.1

View File

@@ -1,10 +1,10 @@
{
"name": "@blitzjs/auth",
"version": "0.0.0",
"version": "2.0.0-alpha.2",
"scripts": {
"build": "unbuild",
"dev": "pnpm run predev && watch unbuild src --wait=0.2",
"predev": "wait-on -d 250 ../blitz/dist/index-server.d.ts",
"dev": "pnpm run predev && watch unbuild src --wait=0.2",
"lint": "eslint . --fix",
"test": "vitest run",
"test-watch": "vitest",
@@ -19,26 +19,8 @@
"files": [
"dist/**"
],
"devDependencies": {
"@blitzjs/config": "workspace:*",
"@testing-library/react": "13.0.0",
"@testing-library/react-hooks": "7.0.2",
"@types/cookie": "0.4.1",
"@types/jsonwebtoken": "8.5.8",
"@types/react": "17.0.43",
"@types/react-dom": "17.0.14",
"react": "18.0.0",
"react-dom": "18.0.0",
"typescript": "^4.5.3",
"unbuild": "0.6.9",
"watch": "1.0.2"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@types/b64-lite": "1.3.0",
"@types/debug": "4.1.7",
"@types/secure-password": "3.1.1",
"b64-lite": "1.4.0",
"bad-behavior": "1.0.1",
@@ -51,5 +33,23 @@
"path": "0.12.7",
"secure-password": "4.0.0",
"url": "0.11.0"
},
"devDependencies": {
"@blitzjs/config": "workspace:*",
"@testing-library/react": "13.0.0",
"@testing-library/react-hooks": "7.0.2",
"@types/cookie": "0.4.1",
"@types/debug": "4.1.7",
"@types/jsonwebtoken": "8.5.8",
"@types/react": "17.0.43",
"@types/react-dom": "17.0.14",
"react": "18.0.0",
"react-dom": "18.0.0",
"typescript": "^4.5.3",
"unbuild": "0.6.9",
"watch": "1.0.2"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -131,7 +131,7 @@ export interface UseSessionOptions {
}
export const useSession = (options: UseSessionOptions = {}): ClientSession => {
const suspense = options?.suspense ?? Boolean(process.env.__BLITZ_SUSPENSE_ENABLED)
const suspense = options?.suspense ?? Boolean(globalThis.__BLITZ_SUSPENSE_ENABLED)
let initialState: ClientSession
if (options.initialPublicData) {

View File

@@ -3,4 +3,5 @@ import {SessionConfig} from "./shared/types"
declare global {
var sessionConfig: SessionConfig
var __BLITZ_SESSION_COOKIE_PREFIX: string | undefined
var __BLITZ_SUSPENSE_ENABLED: boolean
}

View File

@@ -1,6 +1,7 @@
import "./global"
export * from "./client"
export * from "./shared/constants"
export type {
SessionContextBase,
SessionContext,

View File

@@ -0,0 +1,15 @@
# @blitzjs/next
## 2.0.0-alpha.2
### Patch Changes
- @blitzjs/rpc@2.0.0-alpha.2
## 2.0.0-alpha.1
### Patch Changes
- 46a34c7b: initial publish
- Updated dependencies [46a34c7b]
- @blitzjs/rpc@2.0.0-alpha.1

View File

@@ -1,10 +1,10 @@
{
"name": "@blitzjs/next",
"version": "0.0.0",
"version": "2.0.0-alpha.2",
"scripts": {
"build": "unbuild",
"dev": "pnpm predev && pnpm watch unbuild src --wait=0.2",
"predev": "wait-on -d 250 ../blitz/dist/index-server.d.ts",
"predev": "wait-on -d 250 ../blitz/dist/index-server.d.ts && wait-on -d 250 ../blitz-rpc/dist/index-server.d.ts",
"lint": "eslint . --fix",
"test": "vitest run",
"test-watch": "vitest",
@@ -20,8 +20,10 @@
"dist/**"
],
"dependencies": {
"@blitzjs/rpc": "2.0.0-alpha.2",
"debug": "4.3.3",
"react-query": "3.34.12"
"fs-extra": "10.0.1",
"react-query": "3.21.1"
},
"devDependencies": {
"@blitzjs/config": "workspace:*",
@@ -37,7 +39,7 @@
"@types/testing-library__react-hooks": "4.0.0",
"blitz": "workspace:*",
"lodash.frompairs": "4.0.1",
"next": "12.1.4",
"next": "12.1.1",
"react": "18.0.0",
"react-dom": "18.0.0",
"ts-jest": "27.1.4",

View File

@@ -220,8 +220,7 @@ test("withErrorBoundary HOC", () => {
expect(cleanStack(onErrorComponentStack)).toMatchInlineSnapshot(`
{
"componentStack": "
at __vite_ssr_import_4__.withErrorBoundary.FallbackComponent
at ErrorBoundary
at ErrorBoundary
at withErrorBoundary",
}
`)

View File

@@ -0,0 +1,5 @@
import {QueryClient} from "react-query"
declare global {
var queryClient: QueryClient
}

View File

@@ -1,3 +1,4 @@
import "./global"
import type {
ClientPlugin,
BlitzProvider as BlitzProviderType,
@@ -6,8 +7,9 @@ import type {
} from "blitz"
import {AppProps} from "next/app"
import Head from "next/head"
import React, {FC} from "react"
import {Hydrate, HydrateOptions, QueryClient, QueryClientProvider} from "react-query"
import React from "react"
import {QueryClient, QueryClientProvider} from "react-query"
import {Hydrate, HydrateOptions} from "react-query/hydration"
export * from "./error-boundary"
export * from "./error-component"
@@ -36,9 +38,11 @@ const buildWithBlitz = <TPlugins extends readonly ClientPlugin<object>[]>(plugin
return (
<BlitzProvider dehydratedState={props.pageProps?.dehydratedState}>
{/* @ts-ignore todo */}
{props.Component.suppressFirstRenderFlicker && <NoPageFlicker />}
<UserAppRoot {...props} Component={component} />
<>
{/* @ts-ignore todo */}
{props.Component.suppressFirstRenderFlicker && <NoPageFlicker />}
<UserAppRoot {...props} Component={component} />
</>
</BlitzProvider>
)
}
@@ -53,20 +57,27 @@ export type BlitzProviderProps = {
hydrateOptions?: HydrateOptions
}
const BlitzProvider: FC<BlitzProviderProps> = ({
const BlitzProvider = ({
client,
contextSharing = false,
dehydratedState,
hydrateOptions,
children,
}) => {
return (
<QueryClientProvider client={client || queryClient} contextSharing={contextSharing}>
<Hydrate state={dehydratedState} options={hydrateOptions}>
{children}
</Hydrate>
</QueryClientProvider>
)
}: BlitzProviderProps & {children: JSX.Element}) => {
if (globalThis.queryClient) {
return (
<QueryClientProvider
client={client || globalThis.queryClient}
contextSharing={contextSharing}
>
<Hydrate state={dehydratedState} options={hydrateOptions}>
{children}
</Hydrate>
</QueryClientProvider>
)
}
return children
}
export type PluginsExports<TPlugins extends readonly ClientPlugin<object>[]> = Simplify<
@@ -112,34 +123,6 @@ const setupClient = <TPlugins extends readonly ClientPlugin<object>[]>({
export {setupClient}
// ------------------------------------ QUERY CLIENT CODE --------------------------------------------
const initializeQueryClient = () => {
let suspenseEnabled = true
if (!process.env.CLI_COMMAND_CONSOLE && !process.env.CLI_COMMAND_DB) {
suspenseEnabled = Boolean(process.env.__BLITZ_SUSPENSE_ENABLED)
}
return new QueryClient({
defaultOptions: {
queries: {
...(typeof window === "undefined" && {cacheTime: 0}),
suspense: suspenseEnabled,
retry: (failureCount, error: any) => {
if (process.env.NODE_ENV !== "production") return false
// Retry (max. 3 times) only if network error detected
if (error.message === "Network request failed" && failureCount <= 3) return true
return false
},
},
},
})
}
const queryClient = initializeQueryClient()
const customCSS = `
body::before {
content: "";

View File

@@ -73,3 +73,18 @@ export const setupBlitz = ({plugins}: SetupBlitzOptions) => {
return {gSSP, gSP, api}
}
import type {NextConfig} from "next"
import {installWebpackConfig} from "@blitzjs/rpc"
export function withBlitz(nextConfig: NextConfig = {}) {
return Object.assign({}, nextConfig, {
webpack: (config: any, options: any) => {
installWebpackConfig(config)
if (typeof nextConfig.webpack === "function") {
return nextConfig.webpack(config, options)
}
return config
},
} as NextConfig)
}

View File

@@ -0,0 +1,17 @@
# @blitzjs/rpc
## 2.0.0-alpha.2
### Patch Changes
- blitz@2.0.0-alpha.2
- @blitzjs/auth@2.0.0-alpha.2
## 2.0.0-alpha.1
### Patch Changes
- 46a34c7b: initial publish
- Updated dependencies [46a34c7b]
- blitz@2.0.0-alpha.1
- @blitzjs/auth@2.0.0-alpha.1

View File

@@ -1,8 +1,19 @@
import {BuildConfig} from "unbuild"
const config: BuildConfig = {
entries: ["./src/index-browser", "./src/index-server"],
externals: ["index-browser.cjs", "index-browser.mjs", "react"],
entries: [
"./src/index-browser",
"./src/index-server",
"./src/loader-server",
"./src/loader-client",
],
externals: [
"index-browser.cjs",
"index-browser.mjs",
"index-server.cjs",
"index-server.mjs",
"react",
],
declaration: true,
rollup: {
emitCJS: true,

View File

@@ -1,9 +1,10 @@
{
"name": "@blitzjs/rpc",
"version": "0.0.0",
"version": "2.0.0-alpha.2",
"scripts": {
"build": "unbuild",
"dev": "watch unbuild src --wait=0.2",
"predev": "wait-on -d 250 ../blitz/dist/index-server.d.ts && wait-on -d 250 ../blitz-auth/dist/index-browser.d.ts",
"dev": "pnpm run predev && watch unbuild src --wait=0.2",
"lint": "eslint . --fix",
"test": "vitest run",
"test-watch": "vitest",
@@ -18,13 +19,32 @@
"files": [
"dist/**"
],
"dependencies": {},
"dependencies": {
"@blitzjs/auth": "2.0.0-alpha.2",
"b64-lite": "1.4.0",
"bad-behavior": "1.0.1",
"chalk": "^4.1.0",
"debug": "4.3.3",
"react-query": "3.21.1",
"superjson": "1.8.0"
},
"devDependencies": {
"@blitzjs/config": "workspace:*",
"@types/debug": "4.1.7",
"@types/react": "17.0.43",
"@types/react-dom": "17.0.14",
"blitz": "2.0.0-alpha.2",
"next": "12.1.1",
"react": "18.0.0",
"react-dom": "18.0.0",
"typescript": "^4.5.3",
"unbuild": "0.6.9",
"watch": "1.0.2"
},
"peerDependencies": {
"blitz": "2.0.0-alpha.2",
"next": "*"
},
"publishConfig": {
"access": "public"
}

View File

@@ -0,0 +1,13 @@
export * from "./rpc"
export {useQuery, usePaginatedQuery, useInfiniteQuery, useMutation} from "./react-query"
export type {MutateFunction} from "./react-query"
export {
queryClient,
getQueryKey,
getInfiniteQueryKey,
invalidateQuery,
setQueryData,
} from "./react-query-utils"
export {useQueryErrorResetBoundary, QueryClient} from "react-query"
export {dehydrate} from "react-query/hydration"
export {invoke} from "./invoke"

View File

@@ -0,0 +1,21 @@
import {FirstParam, PromiseReturnType, isClient} from "blitz"
import {RpcClient} from "./rpc"
export function invoke<T extends (...args: any) => any, TInput = FirstParam<T>>(
queryFn: T,
params: TInput,
): Promise<PromiseReturnType<T>> {
if (typeof queryFn === "undefined") {
throw new Error(
"invoke is missing the first argument - it must be a query or mutation function",
)
}
if (isClient) {
const fn = queryFn as unknown as RpcClient
return fn(params, {fromInvoke: true}) as ReturnType<T>
} else {
const fn = queryFn as unknown as RpcClient
return fn(params) as ReturnType<T>
}
}

View File

@@ -0,0 +1,208 @@
import {QueryClient, QueryKey} from "react-query"
import {serialize} from "superjson"
import {isClient, isServer, AsyncFunc} from "blitz"
import {ResolverType, RpcClient} from "./rpc"
export type Resolver<TInput, TResult> = (input: TInput, ctx?: any) => Promise<TResult>
type RequestIdleCallbackDeadline = {
readonly didTimeout: boolean
timeRemaining: () => number
}
export const requestIdleCallback =
(typeof self !== "undefined" &&
self.requestIdleCallback &&
self.requestIdleCallback.bind(window)) ||
function (cb: (deadline: RequestIdleCallbackDeadline) => void): NodeJS.Timeout {
let start = Date.now()
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start))
},
})
}, 1)
}
type MutateOptions = {
refetch?: boolean
}
export const initializeQueryClient = () => {
let suspenseEnabled = true
if (!process.env.CLI_COMMAND_CONSOLE && !process.env.CLI_COMMAND_DB) {
suspenseEnabled = Boolean(globalThis.__BLITZ_SUSPENSE_ENABLED)
}
return new QueryClient({
defaultOptions: {
queries: {
...(isServer && {cacheTime: 0}),
suspense: suspenseEnabled,
retry: (failureCount, error: any) => {
if (process.env.NODE_ENV !== "production") return false
// Retry (max. 3 times) only if network error detected
if (error.message === "Network request failed" && failureCount <= 3) return true
return false
},
},
},
})
}
// Create internal QueryClient instance
export const queryClient = initializeQueryClient()
function isRpcClient(f: any): f is RpcClient<any, any> {
return !!f._isRpcClient
}
export interface QueryCacheFunctions<T> {
setQueryData: (
newData: T | ((oldData: T | undefined) => T),
opts?: MutateOptions,
) => ReturnType<typeof setQueryData>
}
export const getQueryCacheFunctions = <TInput, TResult, T extends AsyncFunc>(
resolver: T | Resolver<TInput, TResult> | RpcClient<TInput, TResult>,
params: TInput,
): QueryCacheFunctions<TResult> => ({
setQueryData: (newData, opts = {refetch: true}) => {
return setQueryData(resolver, params, newData, opts)
},
})
export const emptyQueryFn: RpcClient<unknown, unknown> = (() => {
const fn = (() => new Promise(() => {})) as any as RpcClient
fn._isRpcClient = true
return fn
})()
const isNotInUserTestEnvironment = () => {
if (process.env.JEST_WORKER_ID === undefined) return true
if (process.env.BLITZ_TEST_ENVIRONMENT !== undefined) return true
return false
}
export const validateQueryFn = <TInput, TResult>(
queryFn: Resolver<TInput, TResult> | RpcClient<TInput, TResult>,
) => {
if (isClient && !isRpcClient(queryFn) && isNotInUserTestEnvironment()) {
throw new Error(
`Either the file path to your resolver is incorrect (must be in a "queries" or "mutations" folder that isn't nested inside "pages" or "api") or you are trying to use Blitz's useQuery to fetch from third-party APIs (to do that, import useQuery directly from "react-query")`,
)
}
}
const sanitize =
(type: ResolverType) =>
<TInput, TResult>(
queryFn: Resolver<TInput, TResult> | RpcClient<TInput, TResult>,
): RpcClient<TInput, TResult> => {
if (isServer) return queryFn as any
validateQueryFn(queryFn)
const rpcClient = queryFn as RpcClient<TInput, TResult>
const queryFnName = type === "mutation" ? "useMutation" : "useQuery"
if (rpcClient._resolverType !== type && isNotInUserTestEnvironment()) {
throw new Error(
`"${queryFnName}" was expected to be called with a ${type} but was called with a "${rpcClient._resolverType}"`,
)
}
return rpcClient
}
export const sanitizeQuery = sanitize("query")
export const sanitizeMutation = sanitize("mutation")
export const getQueryKeyFromUrlAndParams = (url: string, params: unknown) => {
const queryKey = [url]
const args = typeof params === "function" ? (params as Function)() : params
queryKey.push(serialize(args) as any)
return queryKey as [string, any]
}
export function getQueryKey<TInput, TResult, T extends AsyncFunc>(
resolver: T | Resolver<TInput, TResult> | RpcClient<TInput, TResult>,
params?: TInput,
) {
if (typeof resolver === "undefined") {
throw new Error("getQueryKey is missing the first argument - it must be a resolver function")
}
return getQueryKeyFromUrlAndParams(sanitizeQuery(resolver)._routePath, params)
}
export function getInfiniteQueryKey<TInput, TResult, T extends AsyncFunc>(
resolver: T | Resolver<TInput, TResult> | RpcClient<TInput, TResult>,
params?: TInput,
) {
if (typeof resolver === "undefined") {
throw new Error(
"getInfiniteQueryKey is missing the first argument - it must be a resolver function",
)
}
const queryKey = getQueryKeyFromUrlAndParams(sanitizeQuery(resolver)._routePath, params)
return [...queryKey, "infinite"]
}
export function invalidateQuery<TInput, TResult, T extends AsyncFunc>(
resolver: T | Resolver<TInput, TResult> | RpcClient<TInput, TResult>,
params?: TInput,
) {
if (typeof resolver === "undefined") {
throw new Error(
"invalidateQuery is missing the first argument - it must be a resolver function",
)
}
const fullQueryKey = getQueryKey(resolver, params)
let queryKey: QueryKey
if (params) {
queryKey = fullQueryKey
} else {
// Params not provided, only use first query key item (url)
queryKey = fullQueryKey[0]
}
return queryClient.invalidateQueries(queryKey)
}
export function setQueryData<TInput, TResult, T extends AsyncFunc>(
resolver: T | Resolver<TInput, TResult> | RpcClient<TInput, TResult>,
params: TInput,
newData: TResult | ((oldData: TResult | undefined) => TResult),
opts: MutateOptions = {refetch: true},
): Promise<void | ReturnType<typeof queryClient.invalidateQueries>> {
if (typeof resolver === "undefined") {
throw new Error("setQueryData is missing the first argument - it must be a resolver function")
}
const queryKey = getQueryKey(resolver, params)
return new Promise((res) => {
queryClient.setQueryData(queryKey, newData)
let result: void | ReturnType<typeof queryClient.invalidateQueries>
if (opts.refetch) {
result = invalidateQuery(resolver, params)
}
if (isClient) {
// Fix for https://github.com/blitz-js/blitz/issues/1174
requestIdleCallback(() => {
res(result)
})
} else {
res(result)
}
})
}

View File

@@ -0,0 +1,345 @@
import {
useInfiniteQuery as useInfiniteReactQuery,
UseInfiniteQueryOptions,
UseInfiniteQueryResult,
useQuery as useReactQuery,
UseQueryOptions,
UseQueryResult,
MutateOptions,
useMutation as useReactQueryMutation,
UseMutationOptions,
UseMutationResult,
} from "react-query"
import {useSession} from "@blitzjs/auth"
import {isServer, FirstParam, PromiseReturnType, AsyncFunc} from "blitz"
import {
emptyQueryFn,
getQueryCacheFunctions,
getQueryKey,
QueryCacheFunctions,
sanitizeQuery,
sanitizeMutation,
getInfiniteQueryKey,
} from "./react-query-utils"
import {useRouter} from "next/router"
type QueryLazyOptions = {suspense: unknown} | {enabled: unknown}
type QueryNonLazyOptions =
| {suspense: true; enabled?: never}
| {suspense?: never; enabled: true}
| {suspense: true; enabled: true}
| {suspense?: never; enabled?: never}
// -------------------------
// useQuery
// -------------------------
type RestQueryResult<TResult, TError> = Omit<UseQueryResult<TResult, TError>, "data"> &
QueryCacheFunctions<TResult>
export function useQuery<
T extends AsyncFunc,
TResult = PromiseReturnType<T>,
TError = unknown,
TSelectedData = TResult,
>(
queryFn: T,
params: FirstParam<T>,
options?: UseQueryOptions<TResult, TError, TSelectedData> & QueryNonLazyOptions,
): [TSelectedData, RestQueryResult<TSelectedData, TError>]
export function useQuery<
T extends AsyncFunc,
TResult = PromiseReturnType<T>,
TError = unknown,
TSelectedData = TResult,
>(
queryFn: T,
params: FirstParam<T>,
options: UseQueryOptions<TResult, TError, TSelectedData> & QueryLazyOptions,
): [TSelectedData | undefined, RestQueryResult<TSelectedData, TError>]
export function useQuery<
T extends AsyncFunc,
TResult = PromiseReturnType<T>,
TError = unknown,
TSelectedData = TResult,
>(
queryFn: T,
params: FirstParam<T>,
options: UseQueryOptions<TResult, TError, TSelectedData> = {},
) {
if (typeof queryFn === "undefined") {
throw new Error("useQuery is missing the first argument - it must be a query function")
}
const suspenseEnabled = Boolean(globalThis.__BLITZ_SUSPENSE_ENABLED)
let enabled = isServer && suspenseEnabled ? false : options?.enabled ?? options?.enabled !== null
const suspense = enabled === false ? false : options?.suspense
const session = useSession({suspense})
if (session.isLoading) {
enabled = false
}
const routerIsReady = useRouter().isReady || (isServer && suspenseEnabled)
const enhancedResolverRpcClient = sanitizeQuery(queryFn)
const queryKey = getQueryKey(queryFn, params)
const {data, ...queryRest} = useReactQuery({
queryKey: routerIsReady ? queryKey : ["_routerNotReady_"],
queryFn: routerIsReady
? () => enhancedResolverRpcClient(params, {fromQueryHook: true})
: (emptyQueryFn as any),
...options,
enabled,
})
if (
queryRest.isIdle &&
isServer &&
suspenseEnabled !== false &&
!data &&
(!options || !("suspense" in options) || options.suspense) &&
(!options || !("enabled" in options) || options.enabled)
) {
throw new Promise(() => {})
}
const rest = {
...queryRest,
...getQueryCacheFunctions<FirstParam<T>, TResult, T>(queryFn, params),
}
// return [data, rest as RestQueryResult<TResult>]
return [data, rest]
}
// -------------------------
// usePaginatedQuery
// -------------------------
type RestPaginatedResult<TResult, TError> = Omit<UseQueryResult<TResult, TError>, "data"> &
QueryCacheFunctions<TResult>
export function usePaginatedQuery<
T extends AsyncFunc,
TResult = PromiseReturnType<T>,
TError = unknown,
TSelectedData = TResult,
>(
queryFn: T,
params: FirstParam<T>,
options?: UseQueryOptions<TResult, TError, TSelectedData> & QueryNonLazyOptions,
): [TSelectedData, RestPaginatedResult<TSelectedData, TError>]
export function usePaginatedQuery<
T extends AsyncFunc,
TResult = PromiseReturnType<T>,
TError = unknown,
TSelectedData = TResult,
>(
queryFn: T,
params: FirstParam<T>,
options: UseQueryOptions<TResult, TError, TSelectedData> & QueryLazyOptions,
): [TSelectedData | undefined, RestPaginatedResult<TSelectedData, TError>]
export function usePaginatedQuery<
T extends AsyncFunc,
TResult = PromiseReturnType<T>,
TError = unknown,
TSelectedData = TResult,
>(
queryFn: T,
params: FirstParam<T>,
options: UseQueryOptions<TResult, TError, TSelectedData> = {},
) {
if (typeof queryFn === "undefined") {
throw new Error("usePaginatedQuery is missing the first argument - it must be a query function")
}
const suspenseEnabled = Boolean(globalThis.__BLITZ_SUSPENSE_ENABLED)
let enabled = isServer && suspenseEnabled ? false : options?.enabled ?? options?.enabled !== null
const suspense = enabled === false ? false : options?.suspense
const session = useSession({suspense})
if (session.isLoading) {
enabled = false
}
const routerIsReady = useRouter().isReady || (isServer && suspenseEnabled)
const enhancedResolverRpcClient = sanitizeQuery(queryFn)
const queryKey = getQueryKey(queryFn, params)
const {data, ...queryRest} = useReactQuery({
queryKey: routerIsReady ? queryKey : ["_routerNotReady_"],
queryFn: routerIsReady
? () => enhancedResolverRpcClient(params, {fromQueryHook: true})
: (emptyQueryFn as any),
...options,
keepPreviousData: true,
enabled,
})
if (
queryRest.isIdle &&
isServer &&
suspenseEnabled !== false &&
!data &&
(!options || !("suspense" in options) || options.suspense) &&
(!options || !("enabled" in options) || options.enabled)
) {
throw new Promise(() => {})
}
const rest = {
...queryRest,
...getQueryCacheFunctions<FirstParam<T>, TResult, T>(queryFn, params),
}
// return [data, rest as RestPaginatedResult<TResult>]
return [data, rest]
}
// -------------------------
// useInfiniteQuery
// -------------------------
interface RestInfiniteResult<TResult, TError>
extends Omit<UseInfiniteQueryResult<TResult, TError>, "data">,
QueryCacheFunctions<TResult> {
pageParams: any
}
interface InfiniteQueryConfig<TResult, TError, TSelectedData>
extends UseInfiniteQueryOptions<TResult, TError, TSelectedData, TResult> {
// getPreviousPageParam?: (lastPage: TResult, allPages: TResult[]) => TGetPageParamResult
// getNextPageParam?: (lastPage: TResult, allPages: TResult[]) => TGetPageParamResult
}
export function useInfiniteQuery<
T extends AsyncFunc,
TResult = PromiseReturnType<T>,
TError = unknown,
TSelectedData = TResult,
>(
queryFn: T,
getQueryParams: (pageParam: any) => FirstParam<T>,
options: InfiniteQueryConfig<TResult, TError, TSelectedData> & QueryNonLazyOptions,
): [TSelectedData[], RestInfiniteResult<TSelectedData, TError>]
export function useInfiniteQuery<
T extends AsyncFunc,
TResult = PromiseReturnType<T>,
TError = unknown,
TSelectedData = TResult,
>(
queryFn: T,
getQueryParams: (pageParam: any) => FirstParam<T>,
options: InfiniteQueryConfig<TResult, TError, TSelectedData> & QueryLazyOptions,
): [TSelectedData[] | undefined, RestInfiniteResult<TSelectedData, TError>]
export function useInfiniteQuery<
T extends AsyncFunc,
TResult = PromiseReturnType<T>,
TError = unknown,
TSelectedData = TResult,
>(
queryFn: T,
getQueryParams: (pageParam: any) => FirstParam<T>,
options: InfiniteQueryConfig<TResult, TError, TSelectedData>,
) {
if (typeof queryFn === "undefined") {
throw new Error("useInfiniteQuery is missing the first argument - it must be a query function")
}
const suspenseEnabled = Boolean(globalThis.__BLITZ_SUSPENSE_ENABLED)
let enabled = isServer && suspenseEnabled ? false : options?.enabled ?? options?.enabled !== null
const suspense = enabled === false ? false : options?.suspense
const session = useSession({suspense})
if (session.isLoading) {
enabled = false
}
const routerIsReady = useRouter().isReady || (isServer && suspenseEnabled)
const enhancedResolverRpcClient = sanitizeQuery(queryFn)
const queryKey = getInfiniteQueryKey(queryFn, getQueryParams)
const {data, ...queryRest} = useInfiniteReactQuery({
// we need an extra cache key for infinite loading so that the cache for
// for this query is stored separately since the hook result is an array of results.
// Without this cache for usePaginatedQuery and this will conflict and break.
queryKey: routerIsReady ? queryKey : ["_routerNotReady_"],
queryFn: routerIsReady
? ({pageParam}) =>
enhancedResolverRpcClient(getQueryParams(pageParam), {
fromQueryHook: true,
})
: (emptyQueryFn as any),
...options,
enabled,
})
if (
queryRest.isIdle &&
isServer &&
suspenseEnabled !== false &&
!data &&
(!options || !("suspense" in options) || options.suspense) &&
(!options || !("enabled" in options) || options.enabled)
) {
throw new Promise(() => {})
}
const rest = {
...queryRest,
...getQueryCacheFunctions<FirstParam<T>, TResult, T>(queryFn, getQueryParams),
pageParams: data?.pageParams,
}
return [data?.pages as any, rest]
}
// -------------------------------------------------------------------
// useMutation
// -------------------------------------------------------------------
/*
* We have to override react-query's MutationFunction and MutationResultPair
* types so because we have throwOnError:true by default. And by the RQ types
* have the mutate function result typed as TData|undefined which isn't typed
* properly with throwOnError.
*
* So this fixes that.
*/
export declare type MutateFunction<
TData,
TError = unknown,
TVariables = unknown,
TContext = unknown,
> = (
variables?: TVariables,
config?: MutateOptions<TData, TError, TVariables, TContext>,
) => Promise<TData>
export declare type MutationResultPair<TData, TError, TVariables, TContext> = [
MutateFunction<TData, TError, TVariables, TContext>,
Omit<UseMutationResult<TData, TError>, "mutate" | "mutateAsync">,
]
export declare type MutationFunction<TData, TVariables = unknown> = (
variables: TVariables,
ctx?: any,
) => Promise<TData>
export function useMutation<
TData = unknown,
TError = unknown,
TVariables = void,
TContext = unknown,
>(
mutationResolver: MutationFunction<TData, TVariables>,
config?: UseMutationOptions<TData, TError, TVariables, TContext>,
): MutationResultPair<TData, TError, TVariables, TContext> {
const enhancedResolverRpcClient = sanitizeMutation(mutationResolver)
const {mutate, mutateAsync, ...rest} = useReactQueryMutation<TData, TError, TVariables, TContext>(
(variables) => enhancedResolverRpcClient(variables, {fromQueryHook: true}),
{
throwOnError: true,
...config,
} as any,
)
return [mutateAsync, rest] as MutationResultPair<TData, TError, TVariables, TContext>
}

View File

@@ -0,0 +1,205 @@
import {normalizePathTrailingSlash} from "next/dist/client/normalize-trailing-slash"
import {addBasePath} from "next/dist/shared/lib/router/router"
import {deserialize, serialize} from "superjson"
import {SuperJSONResult} from "superjson/dist/types"
import {isServer, CSRFTokenMismatchError} from "blitz"
import {getQueryKeyFromUrlAndParams, queryClient} from "./react-query-utils"
import {
getAntiCSRFToken,
getPublicDataStore,
HEADER_CSRF,
HEADER_CSRF_ERROR,
HEADER_PUBLIC_DATA_TOKEN,
HEADER_SESSION_CREATED,
} from "@blitzjs/auth"
export function normalizeApiRoute(path: string): string {
return normalizePathTrailingSlash(addBasePath(path))
}
export type ResolverType = "query" | "mutation"
export interface BuildRpcClientParams {
resolverName: string
resolverType: ResolverType
routePath: string
}
export interface RpcOptions {
fromQueryHook?: boolean
fromInvoke?: boolean
alreadySerialized?: boolean
}
export interface EnhancedRpc {
_isRpcClient: true
_resolverType: ResolverType
_resolverName: string
_routePath: string
}
export interface RpcClientBase<Input = unknown, Result = unknown> {
(params: Input, opts?: RpcOptions): Promise<Result>
}
export interface RpcClient<Input = unknown, Result = unknown>
extends EnhancedRpc,
RpcClientBase<Input, Result> {}
// export interface RpcResolver<Input = unknown, Result = unknown> extends EnhancedRpc {
// (params: Input, ctx?: Ctx): Promise<Result>
// }
export function __internal_buildRpcClient({
resolverName,
resolverType,
routePath,
}: BuildRpcClientParams): RpcClient {
const fullRoutePath = normalizeApiRoute("/api/rpc/" + routePath)
const httpClient: RpcClientBase = async (params, opts = {}) => {
const debug = (await import("debug")).default("blitz:rpc")
if (!opts.fromQueryHook && !opts.fromInvoke) {
console.warn(
"[Deprecation] Directly calling queries/mutations is deprecated in favor of invoke(queryFn, params)",
)
}
if (isServer) {
return Promise.resolve() as unknown
}
debug("Starting request for", fullRoutePath, "with", params, "and", opts)
const headers: Record<string, any> = {
"Content-Type": "application/json",
}
const antiCSRFToken = getAntiCSRFToken()
if (antiCSRFToken) {
debug("Adding antiCSRFToken cookie header", antiCSRFToken)
headers[HEADER_CSRF] = antiCSRFToken
} else {
debug("No antiCSRFToken cookie found")
}
let serialized: SuperJSONResult
if (opts.alreadySerialized) {
// params is already serialized with superjson when it gets here
// We have to serialize the params before passing to react-query in the query key
// because otherwise react-query will use JSON.parse(JSON.stringify)
// so by the time the arguments come here the real JS objects are lost
serialized = params as unknown as SuperJSONResult
} else {
serialized = serialize(params)
}
// Create a new AbortController instance for this request
const controller = new AbortController()
const promise = window
.fetch(fullRoutePath, {
method: "POST",
headers,
credentials: "include",
redirect: "follow",
body: JSON.stringify({
params: serialized.json,
meta: {
params: serialized.meta,
},
}),
signal: controller.signal,
})
.then(async (response) => {
debug("Received request for", routePath)
if (response.headers) {
if (response.headers.get(HEADER_PUBLIC_DATA_TOKEN)) {
getPublicDataStore().updateState()
debug("Public data updated")
}
if (response.headers.get(HEADER_SESSION_CREATED)) {
// This also runs on logout, because on logout a new anon session is created
debug("Session created")
setTimeout(async () => {
// Do these in the next tick to prevent various bugs like https://github.com/blitz-js/blitz/issues/2207
debug("Invalidating react-query cache...")
await queryClient.cancelQueries()
await queryClient.resetQueries()
queryClient.getMutationCache().clear()
// We have a 100ms delay here to prevent unnecessary stale queries from running
// This prevents the case where you logout on a page with
// Page.authenticate = {redirectTo: '/login'}
// Without this delay, queries that require authentication on the original page
// will still run (but fail because you are now logged out)
// Ref: https://github.com/blitz-js/blitz/issues/1935
}, 100)
}
if (response.headers.get(HEADER_CSRF_ERROR)) {
const err = new CSRFTokenMismatchError()
err.stack = null!
throw err
}
}
if (!response.ok) {
const error = new Error(response.statusText)
;(error as any).statusCode = response.status
;(error as any).path = routePath
error.stack = null!
throw error
} else {
let payload
try {
payload = await response.json()
} catch (error) {
const err = new Error(`Failed to parse json from ${routePath}`)
err.stack = null!
throw err
}
if (payload.error) {
let error = deserialize({
json: payload.error,
meta: payload.meta?.error,
}) as any
// We don't clear the publicDataStore for anonymous users,
// because there is not sensitive data
if (error.name === "AuthenticationError" && getPublicDataStore().getData().userId) {
getPublicDataStore().clear()
}
const prismaError = error.message.match(/invalid.*prisma.*invocation/i)
if (prismaError && !("code" in error)) {
error = new Error(prismaError[0])
error.statusCode = 500
}
error.stack = null
throw error
} else {
const data = deserialize({
json: payload.result,
meta: payload.meta?.result,
})
if (!opts.fromQueryHook) {
const queryKey = getQueryKeyFromUrlAndParams(routePath, params)
queryClient.setQueryData(queryKey, data)
}
return data
}
}
})
return promise
}
const rpcClient = httpClient as RpcClient
rpcClient._isRpcClient = true
rpcClient._resolverName = resolverName
rpcClient._resolverType = resolverType
rpcClient._routePath = fullRoutePath
return rpcClient
}

View File

@@ -0,0 +1,6 @@
import {QueryClient} from "react-query"
declare global {
var queryClient: QueryClient
var __BLITZ_SUSPENSE_ENABLED: boolean
}

View File

@@ -1 +1,47 @@
export const todo = true
import "./global"
import {createClientPlugin} from "blitz"
import {DefaultOptions, QueryClient} from "react-query"
export * from "./data-client/index"
export const queryClient = globalThis.queryClient
interface BlitzRpcOptions {
reactQueryOptions?: DefaultOptions
}
export const BlitzRpcPlugin = createClientPlugin<BlitzRpcOptions, any>(
({reactQueryOptions}: BlitzRpcOptions) => {
const initializeQueryClient = () => {
let suspenseEnabled = reactQueryOptions?.queries?.suspense ?? true
if (!process.env.CLI_COMMAND_CONSOLE && !process.env.CLI_COMMAND_DB) {
globalThis.__BLITZ_SUSPENSE_ENABLED = suspenseEnabled
}
return new QueryClient({
defaultOptions: {
...reactQueryOptions,
queries: {
...(typeof window === "undefined" && {cacheTime: 0}),
retry: (failureCount, error: any) => {
if (process.env.NODE_ENV !== "production") return false
// Retry (max. 3 times) only if network error detected
if (error.message === "Network request failed" && failureCount <= 3) return true
return false
},
...reactQueryOptions?.queries,
suspense: suspenseEnabled,
},
},
})
}
globalThis.queryClient = initializeQueryClient()
return {
events: {},
middleware: {},
exports: () => {},
}
},
)

View File

@@ -1,3 +1,216 @@
import {assert, Ctx, baseLogger, prettyMs, newLine} from "blitz"
import {NextApiRequest, NextApiResponse} from "next"
import {deserialize, serialize as superjsonSerialize} from "superjson"
import chalk from "chalk"
// TODO - optimize end user server bundles by not exporting all client stuff here
export * from "./index-browser"
export const todoServer = true
// Mechanism used by Vite/Next/Nuxt plugins for automatically loading query and mutation resolvers
function isObject(value: unknown): value is Record<string | symbol, unknown> {
return typeof value === "object" && value !== null
}
function getGlobalObject<T extends Record<string, unknown>>(key: string, defaultValue: T): T {
assert(key.startsWith("__internal_blitz"), "unsupported key")
if (typeof global === "undefined") {
return defaultValue
}
assert(isObject(global), "not an object")
return ((global as Record<string, unknown>)[key] =
((global as Record<string, unknown>)[key] as T) || defaultValue)
}
type Resolver = (...args: unknown[]) => Promise<unknown>
type ResolverFiles = Record<string, () => Promise<{default?: Resolver}>>
// We define `global.__internal_blitzRpcResolverFiles` to ensure we use the same global object.
// Needed for Next.js. I'm guessing that Next.js is including the `node_modules/` files in a seperate bundle than user files.
const g = getGlobalObject<{blitzRpcResolverFilesLoaded: ResolverFiles | null}>(
"__internal_blitzRpcResolverFiles",
{
blitzRpcResolverFilesLoaded: null,
},
)
export function loadBlitzRpcResolverFilesWithInternalMechanism() {
return g.blitzRpcResolverFilesLoaded
}
export function __internal_addBlitzRpcResolver(
routePath: string,
resolver: () => Promise<{default?: Resolver}>,
) {
g.blitzRpcResolverFilesLoaded = g.blitzRpcResolverFilesLoaded || {}
g.blitzRpcResolverFilesLoaded[routePath] = resolver
return resolver
}
import {resolve} from "path"
const dir = __dirname + (() => "")() // trick to avoid `@vercel/ncc` to glob import
const loaderServer = resolve(dir, "./loader-server.cjs")
const loaderClient = resolve(dir, "./loader-client.cjs")
export function installWebpackConfig<T extends any[]>(config: {module?: {rules?: T}}) {
config.module!.rules!.push({
test: /\/\[\[\.\.\.blitz]]\.[jt]s$/,
use: [{loader: loaderServer}],
})
config.module!.rules!.push({
test: /[\\/](queries|mutations)[\\/]/,
use: [{loader: loaderClient}],
})
}
// ----------
// END LOADER
// ----------
async function getResolverMap(): Promise<ResolverFiles | null | undefined> {
// Handles:
// - Next.js
// - Nuxt
// - Vite with `importBuild.js`
{
const resolverFilesLoaded = loadBlitzRpcResolverFilesWithInternalMechanism()
if (resolverFilesLoaded) {
return resolverFilesLoaded
}
}
// Handles:
// - Vite
// {
// const {resolverFilesLoaded, viteProvider} = await loadTelefuncFilesWithVite(runContext)
// if (resolverFilesLoaded) {
// assertUsage(
// Object.keys(resolverFilesLoaded).length > 0,
// getErrMsg(`Vite [\`${viteProvider}\`]`),
// )
// return resolverFilesLoaded
// }
// }
}
interface RpcConfig {
onError?: (error: Error) => void
}
export function rpcHandler(config: RpcConfig) {
return async function handleRpcRequest(req: NextApiRequest, res: NextApiResponse, ctx: Ctx) {
const resolverMap = await getResolverMap()
assert(resolverMap, "No query or mutation resolvers found")
assert(
Array.isArray(req.query.blitz),
"It seems your Blitz RPC endpoint file is not named [[...blitz]].(jt)s. Please ensure it is",
)
const relativeRoutePath = req.query.blitz.join("/")
const routePath = "/" + relativeRoutePath
const loadableResolver = resolverMap[routePath]
if (!loadableResolver) {
throw new Error("No resolver for path: " + routePath)
}
const resolver = (await loadableResolver()).default
if (!resolver) {
throw new Error("No default export for resolver path: " + routePath)
}
const log = baseLogger().getChildLogger({
prefix: [relativeRoutePath + "()"],
})
const customChalk = new chalk.Instance({
level: log.settings.type === "json" ? 0 : chalk.level,
})
if (req.method === "HEAD") {
// We used to initiate database connection here
res.status(200).end()
return
} else if (req.method === "POST") {
// Handle RPC call
if (typeof req.body.params === "undefined") {
const error = {message: "Request body is missing the `params` key"}
log.error(error.message)
res.status(400).json({
result: null,
error,
})
return
}
try {
const data = deserialize({
json: req.body.params,
meta: req.body.meta?.params,
})
log.info(customChalk.dim("Starting with input:"), data ? data : JSON.stringify(data))
const startTime = Date.now()
const result = await resolver(data, (res as any).blitzCtx)
const resolverDuration = Date.now() - startTime
log.debug(customChalk.dim("Result:"), result ? result : JSON.stringify(result))
const serializerStartTime = Date.now()
const serializedResult = superjsonSerialize(result)
const nextSerializerStartTime = Date.now()
;(res as any).blitzResult = result
res.json({
result: serializedResult.json,
error: null,
meta: {
result: serializedResult.meta,
},
})
log.debug(
customChalk.dim(
`Next.js serialization:${prettyMs(Date.now() - nextSerializerStartTime)}`,
),
)
const serializerDuration = Date.now() - serializerStartTime
const duration = Date.now() - startTime
log.info(
customChalk.dim(
`Finished: resolver:${prettyMs(resolverDuration)} serializer:${prettyMs(
serializerDuration,
)} total:${prettyMs(duration)}`,
),
)
newLine()
return
} catch (error: any) {
if (error._clearStack) {
delete error.stack
}
log.error(error)
newLine()
if (!error.statusCode) {
error.statusCode = 500
}
const serializedError = superjsonSerialize(error)
res.json({
result: null,
error: serializedError.json,
meta: {
error: serializedError.meta,
},
})
return
}
} else {
// Everything else is error
log.warn(`${req.method} method not supported`)
res.status(404).end()
return
}
}
}

View File

@@ -0,0 +1,58 @@
import {
assertPosixPath,
convertFilePathToResolverName,
convertFilePathToResolverType,
convertPageFilePathToRoutePath,
toPosixPath,
} from "./loader-utils"
import {assert} from "blitz"
import {posix} from "path"
// Subset of `import type { LoaderDefinitionFunction } from 'webpack'`
type Loader = {
_compiler?: {
name: string
context: string
}
resource: string
cacheable: (enabled: boolean) => void
}
export async function loader(this: Loader, input: string): Promise<string> {
const compiler = this._compiler!
const id = this.resource
const root = this._compiler!.context
const isSSR = compiler.name === "server"
if (!isSSR) {
const code = await transformBlitzRpcResolverClient(input, toPosixPath(id), toPosixPath(root))
return code
}
return input
}
module.exports = loader
export async function transformBlitzRpcResolverClient(_src: string, id: string, root: string) {
assertPosixPath(id)
assertPosixPath(root)
const resolverFilePath = "/" + posix.relative(root, id)
assertPosixPath(resolverFilePath)
const routePath = convertPageFilePathToRoutePath(resolverFilePath)
const resolverName = convertFilePathToResolverName(resolverFilePath)
const resolverType = convertFilePathToResolverType(resolverFilePath)
const code = `
// @ts-nocheck
import { __internal_buildRpcClient } from "@blitzjs/rpc";
export default __internal_buildRpcClient({
resolverName: "${resolverName}",
resolverType: "${resolverType}",
routePath: "${routePath}",
});
`
return code
}

View File

@@ -0,0 +1,109 @@
import {posix, join, dirname} from "path"
import {promises} from "fs"
import {
assertPosixPath,
toPosixPath,
buildPageExtensionRegex,
getIsRpcFile,
topLevelFoldersThatMayContainResolvers,
convertPageFilePathToRoutePath,
} from "./loader-utils"
// Subset of `import type { LoaderDefinitionFunction } from 'webpack'`
type Loader = {
_compiler?: {
name: string
context: string
}
resource: string
cacheable: (enabled: boolean) => void
}
export async function loader(this: Loader, input: string): Promise<string> {
const compiler = this._compiler!
const id = this.resource
const root = this._compiler!.context
const isSSR = compiler.name === "server"
if (isSSR) {
this.cacheable(false)
const resolvers = await collectResolvers(root, ["ts", "js"])
const code = await transformBlitzRpcServer(input, toPosixPath(id), toPosixPath(root), resolvers)
return code
}
return input
}
module.exports = loader
export async function transformBlitzRpcServer(
src: string,
id: string,
root: string,
resolvers: string[],
) {
assertPosixPath(id)
assertPosixPath(root)
const blitzImport = 'import { __internal_addBlitzRpcResolver } from "@blitzjs/rpc";'
// No break line between `blitzImport` and `src` in order to preserve the source map's line mapping
let code = blitzImport + src
code += "\n\n"
for (let resolverFilePath of resolvers) {
const relativeResolverPath = posix.relative(dirname(id), join(root, resolverFilePath))
const routePath = convertPageFilePathToRoutePath(resolverFilePath)
code += `__internal_addBlitzRpcResolver('${routePath}', () => import('${relativeResolverPath}'));`
code += "\n"
}
// console.log("NEW CODE", code)
return code
}
export function collectResolvers(directory: string, pageExtensions: string[]): Promise<string[]> {
return recursiveFindResolvers(directory, buildPageExtensionRegex(pageExtensions))
}
export async function recursiveFindResolvers(
dir: string,
filter: RegExp,
ignore?: RegExp,
arr: string[] = [],
rootDir: string = dir,
): Promise<string[]> {
let folders = await promises.readdir(dir)
if (dir === rootDir) {
folders = folders.filter((folder) => topLevelFoldersThatMayContainResolvers.includes(folder))
}
await Promise.all(
folders.map(async (part: string) => {
const absolutePath = join(dir, part)
if (ignore && ignore.test(part)) return
const pathStat = await promises.stat(absolutePath)
if (pathStat.isDirectory()) {
await recursiveFindResolvers(absolutePath, filter, ignore, arr, rootDir)
return
}
if (!filter.test(part)) {
return
}
const relativeFromRoot = absolutePath.replace(rootDir, "")
if (getIsRpcFile(relativeFromRoot)) {
arr.push(relativeFromRoot)
return
}
}),
)
return arr.sort()
}

View File

@@ -0,0 +1,58 @@
import {assert} from "blitz"
import {win32, posix, sep} from "path"
export function assertPosixPath(path: string) {
const errMsg = `Wrongly formatted path: ${path}`
assert(!path.includes(win32.sep), errMsg)
// assert(path.startsWith('/'), errMsg)
}
export function toPosixPath(path: string) {
if (process.platform !== "win32") {
assert(sep === posix.sep, "TODO")
assertPosixPath(path)
return path
} else {
assert(sep === win32.sep, "TODO")
const pathPosix = path.split(win32.sep).join(posix.sep)
assertPosixPath(pathPosix)
return pathPosix
}
}
export function toSystemPath(path: string) {
path = path.split(posix.sep).join(sep)
path = path.split(win32.sep).join(sep)
return path
}
export const topLevelFoldersThatMayContainResolvers = ["src", "app", "integrations"]
export function buildPageExtensionRegex(pageExtensions: string[]) {
return new RegExp(`(?<!\\.test|\\.spec)\\.(?:${pageExtensions.join("|")})$`)
}
const fileExtensionRegex = /\.([a-z]+)$/
export function convertPageFilePathToRoutePath(filePath: string) {
return filePath
.replace(/^.*?[\\/]queries[\\/]/, "/")
.replace(/^.*?[\\/]mutations[\\/]/, "/")
.replace(fileExtensionRegex, "")
}
export function convertFilePathToResolverName(filePathFromAppRoot: string) {
return filePathFromAppRoot
.replace(/^.*[\\/](queries|mutations)[\\/]/, "")
.replace(fileExtensionRegex, "")
}
export function convertFilePathToResolverType(filePathFromAppRoot: string) {
return filePathFromAppRoot.match(/[\\/]queries[\\/]/) ? "query" : "mutation"
}
export function getIsRpcFile(filePathFromAppRoot: string) {
return (
/[\\/]queries[\\/]/.test(filePathFromAppRoot) || /[\\/]mutations[\\/]/.test(filePathFromAppRoot)
)
}

View File

@@ -1,5 +1,8 @@
{
"extends": "@blitzjs/config/tsconfig.library.json",
"compilerOptions": {
"lib": ["DOM", "ES2015"]
},
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
}

View File

@@ -0,0 +1,16 @@
# blitz
## 2.0.0-alpha.2
### Patch Changes
- Updated dependencies
- @blitzjs/generator@2.0.0-alpha.2
## 2.0.0-alpha.1
### Patch Changes
- 46a34c7b: initial publish
- Updated dependencies [46a34c7b]
- @blitzjs/generator@2.0.0-alpha.1

View File

@@ -2,15 +2,7 @@ import {BuildConfig} from "unbuild"
const config: BuildConfig = {
entries: ["./src/index-browser", "./src/index-server", "./src/cli/index"],
externals: [
"index-browser.cjs",
"index-browser.mjs",
"react",
"chalk",
"console-table-printer",
"tslog",
"ora",
],
externals: ["index-browser.cjs", "index-browser.mjs", "zod"],
declaration: true,
rollup: {
emitCJS: true,

View File

@@ -1,6 +1,6 @@
{
"name": "blitz",
"version": "0.0.0",
"version": "2.0.0-alpha.2",
"scripts": {
"build": "unbuild",
"dev": "watch unbuild src --wait=0.2",
@@ -28,12 +28,19 @@
"chalk": "^4.1.0",
"console-table-printer": "2.10.0",
"cross-spawn": "7.0.3",
"debug": "4.3.3",
"detect-port": "1.3.0",
"dotenv": "16.0.0",
"dotenv-expand": "8.0.3",
"esbuild": "0.14.34",
"fs-extra": "10.0.1",
"hasbin": "1.2.3",
"npm-which": "3.0.1",
"ora": "5.3.0",
"p-event": "4.2.0",
"pkg-dir": "6.0.1",
"prompts": "2.4.2",
"resolve-cwd": "3.0.0",
"superjson": "1.8.0",
"tslog": "3.3.1"
},
@@ -41,7 +48,10 @@
"@blitzjs/config": "workspace:*",
"@types/cookie": "0.4.1",
"@types/cross-spawn": "6.0.2",
"@types/debug": "4.1.7",
"@types/detect-port": "1.3.2",
"@types/express": "4.17.13",
"@types/fs-extra": "9.0.13",
"@types/hasbin": "1.2.0",
"@types/node-fetch": "2.6.1",
"@types/npm-which": "3.0.1",
@@ -50,18 +60,17 @@
"@types/react-dom": "17.0.14",
"@types/test-listen": "1.1.0",
"express": "4.17.3",
"http": "0.0.1-security",
"node-fetch": "3.2.3",
"p-event": "5.0.1",
"pkg-dir": "6.0.1",
"react": "18.0.0",
"resolve-cwd": "3.0.0",
"test-listen": "1.1.0",
"typescript": "^4.5.3",
"unbuild": "0.6.9",
"watch": "1.0.2",
"zod": "3.10.1"
},
"peerDependencies": {
"react": "*"
},
"publishConfig": {
"access": "public"
}

View File

@@ -0,0 +1,9 @@
import {CliCommand} from "../index"
/* @ts-ignore */
import {generateManifest} from "../utils/routes-manifest"
const codegen: CliCommand = async () => {
await generateManifest()
}
export {codegen}

Some files were not shown because too many files have changed in this diff Show More