1
0
mirror of synced 2025-12-19 09:57:57 -05:00

Move internal @blitzjs/core package into nextjs fork core (meta) (#2857)

This commit is contained in:
Brandon Bayer
2021-10-19 18:22:14 -04:00
committed by GitHub
parent f4f6fd91a9
commit 6ac61768d9
78 changed files with 1333 additions and 1406 deletions

View File

@@ -1 +1 @@
12.20.0
14.18.1

View File

@@ -1,8 +1,8 @@
import {render} from "test/utils"
import Home from "./index"
jest.mock("@blitzjs/core", () => ({
...jest.requireActual<object>("@blitzjs/core")!,
jest.mock("next/data-client", () => ({
...jest.requireActual<object>("next/data-client")!,
useQuery: () => [
{
id: 1,
@@ -24,4 +24,4 @@ test("renders blitz documentation link", () => {
const element = getByText(/powered by blitz/i)
// @ts-ignore
expect(element).toBeInTheDocument()
})
})

View File

@@ -1,8 +1,8 @@
import { render } from "test/utils"
import Home from "./index"
jest.mock("@blitzjs/core", () => ({
...jest.requireActual<object>("@blitzjs/core")!,
jest.mock("next/data-client", () => ({
...jest.requireActual<object>("next/data-client")!,
useQuery: () => [
{
id: 1,

View File

@@ -39,7 +39,7 @@
"react": "0.0.0-experimental-6a589ad71",
"react-dom": "0.0.0-experimental-6a589ad71",
"react-final-form": "6.5.2",
"zod": "3.8.1"
"zod": "3.10.1"
},
"devDependencies": {
"@testing-library/cypress": "8.0.1",

1
nextjs/.node-version Normal file
View File

@@ -0,0 +1 @@
14.18.1

View File

@@ -152,10 +152,10 @@ export async function saveRouteManifest(
async function findNodeModulesRoot(src: string) {
/*
* Because of our package structure, and because of how things like pnpm link modules,
* we must first find blitz package, and then find @blitzjs/core and then
* the root of @blitzjs/core
* we must first find blitz package, and then find `next` and then
* the root of `next`
*
* This is because we import from `.blitz` inside @blitzjs/core.
* This is because we import from `.blitz` inside `next/stdlib`.
* If that changes, then this logic here will need to change
*/
manifestDebug('src ' + src)
@@ -189,13 +189,13 @@ async function findNodeModulesRoot(src: string) {
}
const blitzCorePkgLocation = dirname(
(await findUp('package.json', {
cwd: resolveFrom(blitzPkgLocation, '@blitzjs/core'),
cwd: resolveFrom(blitzPkgLocation, 'next'),
})) ?? ''
)
manifestDebug('blitzCorePkgLocation ' + blitzCorePkgLocation)
if (!blitzCorePkgLocation) {
throw new Error(
"Internal Blitz Error: unable to find '@blitzjs/core' package location"
"Internal Blitz Error: unable to find 'next' package location"
)
}
root = join(blitzCorePkgLocation, '../../')

View File

@@ -1,6 +1,9 @@
/* global window */
import React from 'react'
import Router from '../shared/lib/router/router'
import Router, {
extractQueryFromAsPath,
extractRouterParams,
} from '../shared/lib/router/router'
import type { NextRouter } from '../shared/lib/router/router'
import { RouterContext } from '../shared/lib/router-context'
@@ -17,6 +20,7 @@ type SingletonRouterBase = {
export { Router }
export type { NextRouter }
export type BlitzRouter = NextRouter
export type SingletonRouter = SingletonRouterBase & NextRouter
@@ -177,3 +181,86 @@ export function makePublicRouterInstance(router: Router): NextRouter {
return instance
}
export function useRouterQuery() {
const router = useRouter()
const query = React.useMemo(() => {
const query = extractQueryFromAsPath(router.asPath)
return query
}, [router.asPath])
return query
}
type Dict<T> = Record<string, T | undefined>
type ReturnTypes = 'string' | 'number' | 'array'
export function useParams(): Dict<string | string[]>
export function useParams(returnType?: ReturnTypes): Dict<string | string[]>
export function useParams(returnType: 'string'): Dict<string>
export function useParams(returnType: 'number'): Dict<number>
export function useParams(returnType: 'array'): Dict<string[]>
export function useParams(
returnType?: 'string' | 'number' | 'array' | undefined
) {
const router = useRouter()
const query = useRouterQuery()
const params = React.useMemo(() => {
const rawParams = extractRouterParams(router.query, query)
if (returnType === 'string') {
const params: Dict<string> = {}
for (const key in rawParams) {
if (typeof rawParams[key] === 'string') {
params[key] = rawParams[key] as string
}
}
return params
}
if (returnType === 'number') {
const params: Dict<number> = {}
for (const key in rawParams) {
if (rawParams[key]) {
const num = Number(rawParams[key])
params[key] = isNaN(num) ? undefined : num
}
}
return params
}
if (returnType === 'array') {
const params: Dict<string[]> = {}
for (const key in rawParams) {
const rawValue = rawParams[key]
if (Array.isArray(rawParams[key])) {
params[key] = rawValue as string[]
} else if (typeof rawValue === 'string') {
params[key] = [rawValue]
}
}
return params
}
return rawParams
}, [router.query, query, returnType])
return params
}
export function useParam(key: string): undefined | string | string[]
export function useParam(key: string, returnType: 'string'): string | undefined
export function useParam(key: string, returnType: 'number'): number | undefined
export function useParam(key: string, returnType: 'array'): string[] | undefined
export function useParam(
key: string,
returnType?: ReturnTypes
): undefined | number | string | string[] {
const params = useParams(returnType)
const value = params[key]
return value
}

View File

@@ -0,0 +1,22 @@
Copyright 2012-2016 The Dojo Foundation <http://dojofoundation.org/>
Based on Underscore.js, copyright 2009-2016 Jeremy Ashkenas,
DocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1 @@
module.exports=(()=>{var r={460:r=>{function fromPairs(r){var e=-1,_=r?r.length:0,a={};while(++e<_){var t=r[e];a[t[0]]=t[1]}return a}r.exports=fromPairs}};var e={};function __nccwpck_require__(_){if(e[_]){return e[_].exports}var a=e[_]={exports:{}};var t=true;try{r[_](a,a.exports,__nccwpck_require__);t=false}finally{if(t)delete e[_]}return a.exports}__nccwpck_require__.ab=__dirname+"/";return __nccwpck_require__(460)})();

View File

@@ -0,0 +1 @@
{"name":"lodash.frompairs","main":"index.js","author":"John-David Dalton <john.david.dalton@gmail.com> (http://allyoucanleet.com/)","license":"MIT"}

View File

@@ -0,0 +1,6 @@
declare module 'lodash.frompairs' {
// eslint-disable-next-line
export default function fromPairs<T>(
pairs: List<[string, T]> | null | undefined
): Dictionary<T>
}

View File

@@ -0,0 +1,3 @@
declare module 'micromatch' {
export function isMatch(source: string, patterns: string[]): boolean
}

View File

@@ -0,0 +1,176 @@
// Type definitions for npm-which 3.0
// Project: https://github.com/timoxley/npm-which
// Definitions by: Manuel Thalmann <https://github.com/manuth>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
declare module 'npm-which' {
/**
* Provides options for the `npmwhich`-module.
*/
interface NpmWhichOptions {
/**
* The environment to use for resolving the binary.
*/
env?: NodeJS.ProcessEnv
/**
* The directory to find the binary for.
*/
cwd?: string
}
/**
* Provides options for the `npmwhich`-module.
*/
interface StaticWhichOptions {
/**
* The environment to use for resolving the binary.
*/
env?: NodeJS.ProcessEnv
/**
* The directory to find the binary for.
*/
cwd: string
}
/**
* Represents a callback for handling the result of `NpmWhich`.
*/
interface NpmWhichCallback {
/**
* Handles the result of `NpmWhich`.
*
* @param error
* The error-message.
*
* @param result
* The result.
*/
(error: string, result: string): void
}
/**
* Represents a basic interface for `npm-which`.
*/
interface WhichBase<TOptions> {
/**
* Creates a searcher for the specified command.
*
* @param cmd
* The command to look for.
*
* @param options
* The default options.
*
* @return
* A searcher for the specified command.
*/
(cmd: string, options?: TOptions): InnerWhich
/**
* Searches for the specified command.
*
* @param cmd
* The command to look for.
*
* @param callback
* A callback for handling the result.
*/
(cmd: string, callback: NpmWhichCallback): void
/**
* Searches for the specified command.
*
* @param cmd
* The command to look for.
*
* @param options
* The options for searching the command.
*
* @param callback
* A callback for handling the result.
*/
(cmd: string, options: TOptions, callback: NpmWhichCallback): void
}
/**
* Represents the static instance of `npm-which`.
*/
interface StaticWhich extends WhichBase<StaticWhichOptions> {
/**
* Initializes an `NpmWhich`-instance for the specified working-directory.
*
* @param cwd
* The working-directory to browse.
*/
(cwd?: string): NpmWhich
/**
* Searches for the specified command.
*
* @param cmd
* The command to look for.
*
* @param options
* The options for searching the command.
*/
sync(cmd: string, options: StaticWhichOptions): string
}
/**
* Provides the functionality to search for a command.
*/
interface NpmWhich extends WhichBase<NpmWhichOptions> {
/**
* Searches for the specified command.
*
* @param cmd
* The command to look for.
*
* @param options
* The options for searching the command.
*/
sync(cmd: string, options?: StaticWhichOptions): string
}
interface InnerWhich {
/**
* Creates a searcher for the specified command.
*
* @param options
* The options for searching the command.
*/
(options?: NpmWhichOptions): InnerWhich
/**
* Searches for the command.
*
* @param callback
* A callback for handling the result.
*/
(callback: NpmWhichCallback): void
/**
* Searches for the command.
*
* @param options
* The options for searching the command.
*
* @param callback
* A callback for handling the result.
*/
(options: NpmWhichOptions, callback: NpmWhichCallback): void
/**
* Searches for the command.
*
* @param options
* The options for searching the command.
*/
sync(options?: NpmWhichOptions): string
}
let npmWhich: StaticWhich
export = npmWhich
}

View File

@@ -92,6 +92,7 @@
"chokidar": "3.5.1",
"constants-browserify": "1.0.0",
"cookie-session": "^1.4.0",
"cross-spawn": "7.0.3",
"crypto-browserify": "3.12.0",
"cssnano-simple": "3.0.0",
"debug": "4.3.1",
@@ -108,6 +109,7 @@
"node-fetch": "2.6.1",
"node-html-parser": "1.4.9",
"node-libs-browser": "^2.2.1",
"npm-which": "^3.0.1",
"null-loader": "4.0.1",
"os-browserify": "0.3.0",
"p-limit": "3.1.0",
@@ -192,6 +194,7 @@
"@types/fresh": "0.5.0",
"@types/jsonwebtoken": "8.5.0",
"@types/lodash.curry": "4.1.6",
"@types/lodash.frompairs": "4.0.6",
"@types/lru-cache": "5.1.0",
"@types/node-fetch": "2.5.8",
"@types/path-to-regexp": "1.7.0",
@@ -221,7 +224,6 @@
"conf": "5.0.0",
"content-type": "1.0.4",
"cookie": "0.4.1",
"cross-spawn": "7.0.3",
"css-loader": "4.3.0",
"devalue": "2.0.1",
"escape-string-regexp": "2.0.0",
@@ -238,6 +240,7 @@
"jsonwebtoken": "8.5.1",
"loader-utils": "2.0.0",
"lodash.curry": "4.1.1",
"lodash.frompairs": "4.0.1",
"lru-cache": "5.1.1",
"mini-css-extract-plugin": "1.5.0",
"nanoid": "^3.1.20",
@@ -264,7 +267,8 @@
"unistore": "3.4.1",
"web-vitals": "2.1.0",
"webpack": "4.44.1",
"webpack-sources": "1.4.3"
"webpack-sources": "1.4.3",
"zod": "3.10.1"
},
"engines": {
"node": ">=12.0.0"

View File

@@ -28,7 +28,7 @@ async function appGetInitialProps({
return { pageProps }
}
export default class App<P = {}, CP = {}, S = {}> extends React.Component<
export class App<P = {}, CP = {}, S = {}> extends React.Component<
P & AppProps<CP>,
S
> {
@@ -41,3 +41,4 @@ export default class App<P = {}, CP = {}, S = {}> extends React.Component<
return <Component {...pageProps} />
}
}
export default App

View File

@@ -26,7 +26,7 @@ function _getInitialProps({
/**
* `Error` component used for handling errors.
*/
export default class Error<P = {}> extends React.Component<P & ErrorProps> {
export class ErrorComponent<P = {}> extends React.Component<P & ErrorProps> {
static displayName = 'ErrorPage'
static getInitialProps = _getInitialProps
@@ -69,6 +69,7 @@ export default class Error<P = {}> extends React.Component<P & ErrorProps> {
)
}
}
export default ErrorComponent
const styles: { [k: string]: React.CSSProperties } = {
error: {

View File

@@ -31,7 +31,11 @@ import Loadable from '../shared/lib/loadable'
import { LoadableContext } from '../shared/lib/loadable-context'
import postProcess from '../shared/lib/post-process'
import { RouterContext } from '../shared/lib/router-context'
import { NextRouter } from '../shared/lib/router/router'
import {
NextRouter,
extractRouterParams,
extractQueryFromAsPath,
} from '../shared/lib/router/router'
import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic'
import {
AppType,
@@ -74,6 +78,7 @@ class ServerRouter implements NextRouter {
route: string
pathname: string
query: ParsedUrlQuery
params: ParsedUrlQuery
asPath: string
basePath: string
events: any
@@ -103,6 +108,7 @@ class ServerRouter implements NextRouter {
this.route = pathname.replace(/\/$/, '') || '/'
this.pathname = pathname
this.query = query
this.params = extractRouterParams(query, extractQueryFromAsPath(as))
this.asPath = as
this.isFallback = isFallback
this.basePath = basePath

View File

@@ -65,7 +65,7 @@ export function noSSR<P = {}>(
// function dynamic<P = {}, O extends DynamicOptions>(options: O):
export default function dynamic<P = {}>(
export function dynamic<P = {}>(
dynamicOptions: DynamicOptions<P> | Loader<P>,
options?: DynamicOptions<P>
): React.ComponentType<P> {
@@ -130,3 +130,4 @@ export default function dynamic<P = {}>(
return loadableFn(loadableOptions)
}
export default dynamic

View File

@@ -27,18 +27,21 @@ function onlyReactElement(
// Adds support for React.Fragment
if (child.type === React.Fragment) {
return list.concat(
React.Children.toArray(child.props.children).reduce((
fragmentList: Array<React.ReactElement<any>>,
fragmentChild: any // blitz :React.ReactChild
): Array<React.ReactElement<any>> => {
if (
typeof fragmentChild === 'string' ||
typeof fragmentChild === 'number'
) {
return fragmentList
}
return fragmentList.concat(fragmentChild)
}, []) as any //blitz
React.Children.toArray(child.props.children).reduce(
(
fragmentList: Array<React.ReactElement<any>>,
fragmentChild: any // blitz :React.ReactChild
): Array<React.ReactElement<any>> => {
if (
typeof fragmentChild === 'string' ||
typeof fragmentChild === 'number'
) {
return fragmentList
}
return fragmentList.concat(fragmentChild)
},
[]
) as any //blitz
)
}
return list.concat(child)
@@ -144,10 +147,9 @@ function reduceComponents(
c.type === 'link' &&
c.props['href'] &&
// TODO(prateekbh@): Replace this with const from `constants` when the tree shaking works.
[
'https://fonts.googleapis.com/css',
'https://use.typekit.net/',
].some((url) => c.props['href'].startsWith(url))
['https://fonts.googleapis.com/css', 'https://use.typekit.net/'].some(
(url) => c.props['href'].startsWith(url)
)
) {
const newProps = { ...(c.props || {}) }
newProps['data-href'] = newProps['href']
@@ -167,7 +169,7 @@ function reduceComponents(
* This component injects elements to `<head>` of your page.
* To avoid duplicated `tags` in `<head>` you can use the `key` property, which will make sure every tag is only rendered once.
*/
function Head({ children }: { children: React.ReactNode }) {
export function Head({ children }: { children: React.ReactNode }) {
const ampState = useContext(AmpStateContext)
const headManager = useContext(HeadManagerContext)
return (

View File

@@ -34,6 +34,7 @@ import { searchParamsToUrlQuery } from './utils/querystring'
import resolveRewrites from './utils/resolve-rewrites'
import { getRouteMatcher } from './utils/route-matcher'
import { getRouteRegex } from './utils/route-regex'
import fromPairs from 'next/dist/compiled/lodash.frompairs'
declare global {
interface Window {
@@ -405,6 +406,7 @@ export type BaseRouter = {
route: string
pathname: string
query: ParsedUrlQuery
params: ParsedUrlQuery
asPath: string
basePath: string
locale?: string
@@ -529,6 +531,7 @@ export default class Router implements BaseRouter {
route: string
pathname: string
query: ParsedUrlQuery
params: ParsedUrlQuery
asPath: string
basePath: string
@@ -630,6 +633,7 @@ export default class Router implements BaseRouter {
this.pageLoader = pageLoader
this.pathname = pathname
this.query = query
this.params = extractRouterParams(query, extractQueryFromAsPath(as))
// if auto prerendered and dynamic route wait to update asPath
// until after mount to prevent hydration mismatch
const autoExportDynamic =
@@ -1442,6 +1446,7 @@ export default class Router implements BaseRouter {
this.route = route
this.pathname = pathname
this.query = query
this.params = extractRouterParams(query, extractQueryFromAsPath(as))
this.asPath = as
return this.notify(data, resetScroll)
}
@@ -1709,3 +1714,75 @@ export default class Router implements BaseRouter {
)
}
}
/*
* Based on the code of https://github.com/lukeed/qss
*/
const decodeString = (str: string) =>
decodeURIComponent(str.replace(/\+/g, '%20'))
function decode(str: string) {
if (!str) return {}
let out: Record<string, string | string[]> = {}
for (const current of str.split('&')) {
let [key, value = ''] = current.split('=')
key = decodeString(key)
value = decodeString(value)
if (key.length === 0) continue
if (key in out) {
out[key] = ([] as string[]).concat(out[key], value)
} else {
out[key] = value
}
}
return out
}
type ParsedUrlQueryValue = string | string[] | undefined
function areQueryValuesEqual(
value1: ParsedUrlQueryValue,
value2: ParsedUrlQueryValue
) {
// Check if their type match
if (typeof value1 !== typeof value2) {
return false
}
if (Array.isArray(value1) && Array.isArray(value2)) {
if (value1.length !== value2.length) {
return false
}
for (let i = 0; i < value1.length; i++) {
if (value1[i] !== value2[i]) {
return false
}
}
return true
}
return value1 === value2
}
export function extractQueryFromAsPath(asPath: string) {
return decode(asPath.split('?', 2)[1])
}
export function extractRouterParams(
routerQuery: ParsedUrlQuery,
asPathQuery: ParsedUrlQuery
) {
return fromPairs(
Object.entries(routerQuery).filter(
([key, value]) =>
typeof asPathQuery[key] === 'undefined' ||
!areQueryValuesEqual(value, asPathQuery[key])
)
)
}

View File

@@ -1,8 +1,9 @@
let runtimeConfig: any
export default () => {
export const getConfig = () => {
return runtimeConfig
}
export default getConfig
export function setConfig(configValue: any): void {
runtimeConfig = configValue

View File

@@ -6,6 +6,7 @@ export * from './middleware'
export * from './auth-sessions'
export * from './auth-utils'
export * from './passport-adapter'
export * from './resolver'
export function isLocalhost(req: NextApiRequest | IncomingMessage): boolean {
let { host } = req.headers

View File

@@ -1,6 +1,12 @@
import {AuthenticatedSessionContext, Ctx, SessionContext, SessionContextBase} from "next/types"
import {Await, EnsurePromise} from "next/types/utils"
import type {input as zInput, output as zOutput, ZodTypeAny} from "zod"
import {
AuthenticatedSessionContext,
Ctx,
SessionContext,
SessionContextBase,
} from 'next/types'
import { Await, EnsurePromise } from 'next/types/utils'
import type { input as zInput, output as zOutput, ZodTypeAny } from 'zod'
import { ParserType } from '../types/index'
interface ResultWithContext<Result = unknown, Context = unknown> {
__blitz: true
@@ -9,49 +15,84 @@ interface ResultWithContext<Result = unknown, Context = unknown> {
}
function isResultWithContext(x: unknown): x is ResultWithContext {
return (
typeof x === "object" && x !== null && "ctx" in x && (x as ResultWithContext).__blitz === true
typeof x === 'object' &&
x !== null &&
'ctx' in x &&
(x as ResultWithContext).__blitz === true
)
}
export interface AuthenticatedMiddlewareCtx extends Omit<Ctx, "session"> {
export interface AuthenticatedMiddlewareCtx extends Omit<Ctx, 'session'> {
session: AuthenticatedSessionContext
}
type PipeFn<Prev, Next, PrevCtx, NextCtx = PrevCtx> = (
i: Await<Prev>,
c: PrevCtx,
) => Next extends ResultWithContext ? never : Next | ResultWithContext<Next, NextCtx>
c: PrevCtx
) => Next extends ResultWithContext
? never
: Next | ResultWithContext<Next, NextCtx>
function pipe<A, Z>(ab: (i: A, c: Ctx) => Z): (input: A, ctx: Ctx) => EnsurePromise<Z>
function pipe<A, Z>(
ab: (i: A, c: Ctx) => Z
): (input: A, ctx: Ctx) => EnsurePromise<Z>
function pipe<A, B, C, CA = Ctx, CB = CA, CC = CB>(
ab: PipeFn<A, B, CA, CB>,
bc: PipeFn<B, C, CB, CC>,
bc: PipeFn<B, C, CB, CC>
): (input: A, ctx: CA) => EnsurePromise<C>
function pipe<A, B, C, D, CA = Ctx, CB = CA, CC = CB, CD = CC>(
ab: PipeFn<A, B, CA, CB>,
bc: PipeFn<B, C, CB, CC>,
cd: PipeFn<C, D, CC, CD>,
cd: PipeFn<C, D, CC, CD>
): (input: A, ctx: CA) => EnsurePromise<D>
function pipe<A, B, C, D, E, CA = Ctx, CB = CA, CC = CB, CD = CC, CE = CD>(
ab: PipeFn<A, B, CA, CB>,
bc: PipeFn<B, C, CB, CC>,
cd: PipeFn<C, D, CC, CD>,
de: PipeFn<D, E, CD, CE>,
de: PipeFn<D, E, CD, CE>
): (input: A, ctx: CA) => EnsurePromise<E>
function pipe<A, B, C, D, E, F, CA = Ctx, CB = CA, CC = CB, CD = CC, CE = CD, CF = CE>(
function pipe<
A,
B,
C,
D,
E,
F,
CA = Ctx,
CB = CA,
CC = CB,
CD = CC,
CE = CD,
CF = CE
>(
ab: PipeFn<A, B, CA, CB>,
bc: PipeFn<B, C, CB, CC>,
cd: PipeFn<C, D, CC, CD>,
de: PipeFn<D, E, CD, CE>,
ef: PipeFn<E, F, CE, CF>,
ef: PipeFn<E, F, CE, CF>
): (input: A, ctx: CA) => EnsurePromise<F>
function pipe<A, B, C, D, E, F, G, CA = Ctx, CB = CA, CC = CB, CD = CC, CE = CD, CF = CE, CG = CF>(
function pipe<
A,
B,
C,
D,
E,
F,
G,
CA = Ctx,
CB = CA,
CC = CB,
CD = CC,
CE = CD,
CF = CE,
CG = CF
>(
ab: PipeFn<A, B, CA, CB>,
bc: PipeFn<B, C, CB, CC>,
cd: PipeFn<C, D, CC, CD>,
de: PipeFn<D, E, CD, CE>,
ef: PipeFn<E, F, CE, CF>,
fg: PipeFn<F, G, CF, CG>,
fg: PipeFn<F, G, CF, CG>
): (input: A, ctx: CA) => EnsurePromise<CG>
function pipe<
A,
@@ -77,7 +118,7 @@ function pipe<
de: PipeFn<D, E, CD, CE>,
ef: PipeFn<E, F, CE, CF>,
fg: PipeFn<F, G, CF, CG>,
gh: PipeFn<G, H, CG, CH>,
gh: PipeFn<G, H, CG, CH>
): (input: A, ctx: CA) => EnsurePromise<H>
function pipe<
A,
@@ -106,7 +147,7 @@ function pipe<
ef: PipeFn<E, F, CE, CF>,
fg: PipeFn<F, G, CF, CG>,
gh: PipeFn<G, H, CG, CH>,
hi: PipeFn<H, I, CH, CI>,
hi: PipeFn<H, I, CH, CI>
): (input: A, ctx: CA) => EnsurePromise<I>
function pipe<
A,
@@ -138,7 +179,7 @@ function pipe<
fg: PipeFn<F, G, CF, CG>,
gh: PipeFn<G, H, CG, CH>,
hi: PipeFn<H, I, CH, CI>,
ij: PipeFn<I, J, CI, CJ>,
ij: PipeFn<I, J, CI, CJ>
): (input: A, ctx: CA) => EnsurePromise<J>
function pipe<
A,
@@ -173,7 +214,7 @@ function pipe<
gh: PipeFn<G, H, CG, CH>,
hi: PipeFn<H, I, CH, CI>,
ij: PipeFn<I, J, CI, CJ>,
jk: PipeFn<J, K, CJ, CK>,
jk: PipeFn<J, K, CJ, CK>
): (input: A, ctx: CA) => EnsurePromise<K>
function pipe<
A,
@@ -211,7 +252,7 @@ function pipe<
hi: PipeFn<H, I, CH, CI>,
ij: PipeFn<I, J, CI, CJ>,
jk: PipeFn<J, K, CJ, CK>,
kl: PipeFn<K, L, CK, CL>,
kl: PipeFn<K, L, CK, CL>
): (input: A, ctx: CA) => EnsurePromise<L>
function pipe<
A,
@@ -252,7 +293,7 @@ function pipe<
ij: PipeFn<I, J, CI, CJ>,
jk: PipeFn<J, K, CJ, CK>,
kl: PipeFn<K, L, CK, CL>,
lm: PipeFn<L, M, CL, CM>,
lm: PipeFn<L, M, CL, CM>
): (input: A, ctx: CA) => EnsurePromise<M>
function pipe(...args: unknown[]): unknown {
const functions = args as PipeFn<unknown, unknown, Ctx>[]
@@ -271,9 +312,9 @@ function pipe(...args: unknown[]): unknown {
}
interface ResolverAuthorize {
<T, C = Ctx>(...args: Parameters<SessionContextBase["$authorize"]>): (
<T, C = Ctx>(...args: Parameters<SessionContextBase['$authorize']>): (
input: T,
ctx: C,
ctx: C
) => ResultWithContext<T, AuthenticatedMiddlewareCtx>
}
@@ -290,24 +331,30 @@ const authorize: ResolverAuthorize = (...args) => {
}
}
export type ParserType = "sync" | "async"
function zod<Schema extends ZodTypeAny, InputType = zInput<Schema>, OutputType = zOutput<Schema>>(
function zod<
Schema extends ZodTypeAny,
InputType = zInput<Schema>,
OutputType = zOutput<Schema>
>(schema: Schema, parserType: 'sync'): (input: InputType) => OutputType
function zod<
Schema extends ZodTypeAny,
InputType = zInput<Schema>,
OutputType = zOutput<Schema>
>(
schema: Schema,
parserType: "sync",
): (input: InputType) => OutputType
function zod<Schema extends ZodTypeAny, InputType = zInput<Schema>, OutputType = zOutput<Schema>>(
schema: Schema,
parserType: "async",
parserType: 'async'
): (input: InputType) => Promise<OutputType>
function zod<Schema extends ZodTypeAny, InputType = zInput<Schema>, OutputType = zOutput<Schema>>(
schema: Schema,
): (input: InputType) => Promise<OutputType>
function zod<Schema extends ZodTypeAny, InputType = zInput<Schema>, OutputType = zOutput<Schema>>(
schema: Schema,
parserType: ParserType = "async",
) {
if (parserType === "sync") {
function zod<
Schema extends ZodTypeAny,
InputType = zInput<Schema>,
OutputType = zOutput<Schema>
>(schema: Schema): (input: InputType) => Promise<OutputType>
function zod<
Schema extends ZodTypeAny,
InputType = zInput<Schema>,
OutputType = zOutput<Schema>
>(schema: Schema, parserType: ParserType = 'async') {
if (parserType === 'sync') {
return (input: InputType): OutputType => schema.parse(input)
} else {
return (input: InputType): Promise<OutputType> => schema.parseAsync(input)

View File

@@ -1,12 +1,16 @@
import {getPublicDataStore, useAuthorizeIf, useSession} from "next/data-client"
import {BlitzProvider} from "next/data-client"
import {formatWithValidation} from "next/dist/shared/lib/utils"
import {RedirectError} from "next/stdlib"
import {AppProps, BlitzPage} from "next/types"
import React, {ComponentPropsWithoutRef, useEffect} from "react"
import SuperJSON from "superjson"
import {Head} from "./head"
import {clientDebug} from "./utils"
import {
getPublicDataStore,
useAuthorizeIf,
useSession,
} from '../data-client/auth'
import { BlitzProvider } from '../data-client/react-query'
import { formatWithValidation } from '../shared/lib/utils'
import { Head } from '../shared/lib/head'
import { RedirectError } from './errors'
import { AppProps, BlitzPage } from '../types/index'
import React, { ComponentPropsWithoutRef, useEffect } from 'react'
import SuperJSON from 'superjson'
const debug = require('debug')('blitz:approot')
const customCSS = `
body::before {
@@ -34,15 +38,18 @@ const noscriptCSS = `
const NoPageFlicker = () => {
return (
<Head>
<style dangerouslySetInnerHTML={{__html: customCSS}} />
<style dangerouslySetInnerHTML={{ __html: customCSS }} />
<noscript>
<style dangerouslySetInnerHTML={{__html: noscriptCSS}} />
<style dangerouslySetInnerHTML={{ __html: noscriptCSS }} />
</noscript>
</Head>
)
}
function getAuthValues(Page: BlitzPage, props: ComponentPropsWithoutRef<BlitzPage>) {
function getAuthValues(
Page: BlitzPage,
props: ComponentPropsWithoutRef<BlitzPage>
) {
let authenticate = Page.authenticate
let redirectAuthenticatedTo = Page.redirectAuthenticatedTo
@@ -54,7 +61,10 @@ function getAuthValues(Page: BlitzPage, props: ComponentPropsWithoutRef<BlitzPag
while (true) {
const type = layout.type
if (type.authenticate !== undefined || type.redirectAuthenticatedTo !== undefined) {
if (
type.authenticate !== undefined ||
type.redirectAuthenticatedTo !== undefined
) {
authenticate = type.authenticate
redirectAuthenticatedTo = type.redirectAuthenticatedTo
break
@@ -69,51 +79,57 @@ function getAuthValues(Page: BlitzPage, props: ComponentPropsWithoutRef<BlitzPag
}
}
return {authenticate, redirectAuthenticatedTo}
return { authenticate, redirectAuthenticatedTo }
}
export function withBlitzInnerWrapper(Page: BlitzPage) {
function withBlitzInnerWrapper(Page: BlitzPage) {
const BlitzInnerRoot = (props: ComponentPropsWithoutRef<BlitzPage>) => {
// We call useSession so this will rerender anytime session changes
useSession({suspense: false})
useSession({ suspense: false })
let {authenticate, redirectAuthenticatedTo} = getAuthValues(Page, props)
let { authenticate, redirectAuthenticatedTo } = getAuthValues(Page, props)
useAuthorizeIf(authenticate === true)
if (typeof window !== "undefined") {
if (typeof window !== 'undefined') {
const publicData = getPublicDataStore().getData()
// We read directly from publicData.userId instead of useSession
// so we can access userId on first render. useSession is always empty on first render
if (publicData.userId) {
clientDebug("[BlitzInnerRoot] logged in")
debug('[BlitzInnerRoot] logged in')
if (typeof redirectAuthenticatedTo === "function") {
redirectAuthenticatedTo = redirectAuthenticatedTo({session: publicData})
if (typeof redirectAuthenticatedTo === 'function') {
redirectAuthenticatedTo = redirectAuthenticatedTo({
session: publicData,
})
}
if (redirectAuthenticatedTo) {
const redirectUrl =
typeof redirectAuthenticatedTo === "string"
typeof redirectAuthenticatedTo === 'string'
? redirectAuthenticatedTo
: formatWithValidation(redirectAuthenticatedTo)
clientDebug("[BlitzInnerRoot] redirecting to", redirectUrl)
debug('[BlitzInnerRoot] redirecting to', redirectUrl)
const error = new RedirectError(redirectUrl)
error.stack = null!
throw error
}
} else {
clientDebug("[BlitzInnerRoot] logged out")
if (authenticate && typeof authenticate === "object" && authenticate.redirectTo) {
let {redirectTo} = authenticate
if (typeof redirectTo !== "string") {
debug('[BlitzInnerRoot] logged out')
if (
authenticate &&
typeof authenticate === 'object' &&
authenticate.redirectTo
) {
let { redirectTo } = authenticate
if (typeof redirectTo !== 'string') {
redirectTo = formatWithValidation(redirectTo)
}
const url = new URL(redirectTo, window.location.href)
url.searchParams.append("next", window.location.pathname)
clientDebug("[BlitzInnerRoot] redirecting to", url.toString())
url.searchParams.append('next', window.location.pathname)
debug('[BlitzInnerRoot] redirecting to', url.toString())
const error = new RedirectError(url.toString())
error.stack = null!
throw error
@@ -126,7 +142,7 @@ export function withBlitzInnerWrapper(Page: BlitzPage) {
for (let [key, value] of Object.entries(Page)) {
;(BlitzInnerRoot as any)[key] = value
}
if (process.env.NODE_ENV !== "production") {
if (process.env.NODE_ENV !== 'production') {
BlitzInnerRoot.displayName = `BlitzInnerRoot`
}
return BlitzInnerRoot
@@ -134,9 +150,15 @@ export function withBlitzInnerWrapper(Page: BlitzPage) {
export function withBlitzAppRoot(UserAppRoot: React.ComponentType<any>) {
const BlitzOuterRoot = (props: AppProps) => {
const component = React.useMemo(() => withBlitzInnerWrapper(props.Component), [props.Component])
const component = React.useMemo(
() => withBlitzInnerWrapper(props.Component),
[props.Component]
)
const {authenticate, redirectAuthenticatedTo} = getAuthValues(props.Component, props.pageProps)
const { authenticate, redirectAuthenticatedTo } = getAuthValues(
props.Component,
props.pageProps
)
const noPageFlicker =
props.Component.suppressFirstRenderFlicker ||
@@ -144,15 +166,15 @@ export function withBlitzAppRoot(UserAppRoot: React.ComponentType<any>) {
redirectAuthenticatedTo
useEffect(() => {
document.documentElement.classList.add("blitz-first-render-complete")
document.documentElement.classList.add('blitz-first-render-complete')
}, [])
let {dehydratedState, _superjson} = props.pageProps
let { dehydratedState, _superjson } = props.pageProps
if (dehydratedState && _superjson) {
const deserializedProps = SuperJSON.deserialize({
json: {dehydratedState},
json: { dehydratedState },
meta: _superjson,
}) as {dehydratedState: any}
}) as { dehydratedState: any }
dehydratedState = deserializedProps?.dehydratedState
}

View File

@@ -1,42 +1,44 @@
import {Router} from "next/router"
import {RedirectError} from "next/stdlib"
import * as React from "react"
import {RouterContext} from "./router"
import {clientDebug} from "./utils"
import { Router } from '../client/router'
import { RedirectError } from './errors'
import * as React from 'react'
import { RouterContext } from '../shared/lib/router-context'
const debug = require('debug')('blitz:errorboundary')
const changedArray = (a: Array<unknown> = [], b: Array<unknown> = []) =>
//eslint-disable-next-line es5/no-es6-static-methods
a.length !== b.length || a.some((item, index) => !Object.is(item, b[index]))
interface FallbackProps {
error: Error
interface ErrorFallbackProps {
error: Error & Record<any, any>
resetErrorBoundary: (...args: Array<unknown>) => void
}
interface ErrorBoundaryPropsWithComponent {
onResetKeysChange?: (
prevResetKeys: Array<unknown> | undefined,
resetKeys: Array<unknown> | undefined,
resetKeys: Array<unknown> | undefined
) => void
onReset?: (...args: Array<unknown>) => void
onError?: (error: Error, info: {componentStack: string}) => void
onError?: (error: Error, info: { componentStack: string }) => void
resetKeys?: Array<unknown>
fallback?: never
FallbackComponent: React.ComponentType<FallbackProps>
FallbackComponent: React.ComponentType<ErrorFallbackProps>
fallbackRender?: never
}
declare function FallbackRender(
props: FallbackProps,
): React.ReactElement<unknown, string | React.FunctionComponent | typeof React.Component> | null
props: ErrorFallbackProps
): React.ReactElement<
unknown,
string | React.FunctionComponent | typeof React.Component
> | null
interface ErrorBoundaryPropsWithRender {
onResetKeysChange?: (
prevResetKeys: Array<unknown> | undefined,
resetKeys: Array<unknown> | undefined,
resetKeys: Array<unknown> | undefined
) => void
onReset?: (...args: Array<unknown>) => void
onError?: (error: Error, info: {componentStack: string}) => void
onError?: (error: Error, info: { componentStack: string }) => void
resetKeys?: Array<unknown>
fallback?: never
FallbackComponent?: never
@@ -46,10 +48,10 @@ interface ErrorBoundaryPropsWithRender {
interface ErrorBoundaryPropsWithFallback {
onResetKeysChange?: (
prevResetKeys: Array<unknown> | undefined,
resetKeys: Array<unknown> | undefined,
resetKeys: Array<unknown> | undefined
) => void
onReset?: (...args: Array<unknown>) => void
onError?: (error: Error, info: {componentStack: string}) => void
onError?: (error: Error, info: { componentStack: string }) => void
resetKeys?: Array<unknown>
fallback: React.ReactElement<
unknown,
@@ -64,9 +66,9 @@ type ErrorBoundaryProps =
| ErrorBoundaryPropsWithComponent
| ErrorBoundaryPropsWithRender
type ErrorBoundaryState = {error: Error | null}
type ErrorBoundaryState = { error: Error | null }
const initialState: ErrorBoundaryState = {error: null}
const initialState: ErrorBoundaryState = { error: null }
class ErrorBoundary extends React.Component<
React.PropsWithRef<React.PropsWithChildren<ErrorBoundaryProps>>,
@@ -75,7 +77,7 @@ class ErrorBoundary extends React.Component<
static contextType = RouterContext
static getDerivedStateFromError(error: Error) {
return {error}
return { error }
}
state = initialState
@@ -92,7 +94,7 @@ class ErrorBoundary extends React.Component<
async componentDidCatch(error: Error, info: React.ErrorInfo) {
if (error instanceof RedirectError) {
clientDebug("Redirecting from ErrorBoundary to", error.url)
debug('Redirecting from ErrorBoundary to', error.url)
await (this.context as Router)?.push(error.url)
return
}
@@ -100,29 +102,35 @@ class ErrorBoundary extends React.Component<
}
componentDidMount() {
const {error} = this.state
const { error } = this.state
if (error !== null) {
this.updatedWithError = true
}
// Automatically reset on route change
;(this.context as Router)?.events?.on("routeChangeComplete", this.handleRouteChange)
;(this.context as Router)?.events?.on(
'routeChangeComplete',
this.handleRouteChange
)
}
handleRouteChange = () => {
clientDebug("Resetting error boundary on route change")
debug('Resetting error boundary on route change')
this.props.onReset?.()
this.reset()
}
componentWillUnmount() {
;(this.context as Router)?.events?.off("routeChangeComplete", this.handleRouteChange)
;(this.context as Router)?.events?.off(
'routeChangeComplete',
this.handleRouteChange
)
}
componentDidUpdate(prevProps: ErrorBoundaryProps) {
const {error} = this.state
const {resetKeys} = this.props
const { error } = this.state
const { resetKeys } = this.props
// There's an edge case where if the thing that triggered the error
// happens to *also* be in the resetKeys array, we'd end up resetting
@@ -142,9 +150,9 @@ class ErrorBoundary extends React.Component<
}
render() {
const {error} = this.state
const { error } = this.state
const {fallbackRender, FallbackComponent, fallback} = this.props
const { fallbackRender, FallbackComponent, fallback } = this.props
if (error !== null) {
const props = {
@@ -156,13 +164,13 @@ class ErrorBoundary extends React.Component<
return null
} else if (React.isValidElement(fallback)) {
return fallback
} else if (typeof fallbackRender === "function") {
} else if (typeof fallbackRender === 'function') {
return fallbackRender(props)
} else if (FallbackComponent) {
return <FallbackComponent {...props} />
} else {
throw new Error(
"<ErrorBoundary> requires either a fallback, fallbackRender, or FallbackComponent prop",
'<ErrorBoundary> requires either a fallback, fallbackRender, or FallbackComponent prop'
)
}
}
@@ -173,7 +181,7 @@ class ErrorBoundary extends React.Component<
function withErrorBoundary<P>(
Component: React.ComponentType<P>,
errorBoundaryProps: ErrorBoundaryProps,
errorBoundaryProps: ErrorBoundaryProps
): React.ComponentType<P> {
const Wrapped: React.ComponentType<P> = (props) => {
return (
@@ -184,7 +192,7 @@ function withErrorBoundary<P>(
}
// Format for display in DevTools
const name = Component.displayName || Component.name || "Unknown"
const name = Component.displayName || Component.name || 'Unknown'
Wrapped.displayName = `withErrorBoundary(${name})`
return Wrapped
@@ -197,9 +205,9 @@ function useErrorHandler(givenError?: unknown): (error: unknown) => void {
return setError
}
export {ErrorBoundary, withErrorBoundary, useErrorHandler}
export { ErrorBoundary, withErrorBoundary, useErrorHandler }
export type {
FallbackProps,
ErrorFallbackProps,
ErrorBoundaryPropsWithComponent,
ErrorBoundaryPropsWithRender,
ErrorBoundaryPropsWithFallback,

View File

@@ -1,4 +1,21 @@
export { Routes } from '.blitz'
export * from './errors'
export * from './zod-utils'
export * from './prisma-utils'
export * from './error-boundary'
export * from './blitz-app-root'
export {
default as Router,
BlitzRouter,
SingletonRouter,
RouterEvent,
withRouter,
useRouter,
useRouterQuery,
useParams,
useParam,
} from '../client/router'
export { RouterContext } from '../shared/lib/router-context'
export const isServer = typeof window === 'undefined'
export const isClient = typeof window !== 'undefined'

View File

@@ -0,0 +1,66 @@
import { spawn } from 'cross-spawn'
import which from 'npm-which'
interface Constructor<T = unknown> {
new (...args: never[]): T
}
interface EnhancedPrismaClientAddedMethods {
$reset: () => Promise<void>
}
interface EnhancedPrismaClientConstructor<
TPrismaClientCtor extends Constructor
> {
new (
...args: ConstructorParameters<TPrismaClientCtor>
): InstanceType<TPrismaClientCtor> & EnhancedPrismaClientAddedMethods
}
export const enhancePrisma = <TPrismaClientCtor extends Constructor>(
client: TPrismaClientCtor
): EnhancedPrismaClientConstructor<TPrismaClientCtor> => {
return new Proxy(
client as EnhancedPrismaClientConstructor<TPrismaClientCtor>,
{
construct(target, args) {
if (
typeof window !== 'undefined' &&
process.env.JEST_WORKER_ID === undefined
) {
// Return object with $use method if in the browser
// Skip in Jest tests because window is defined in Jest tests
return { $use: () => {} }
}
if (!global._blitz_prismaClient) {
const client = new target(...(args as any))
client.$reset = async function reset() {
if (process.env.NODE_ENV === 'production') {
throw new Error(
"You are calling db.$reset() in a production environment. We think you probably didn't mean to do that, so we are throwing this error instead of destroying your life's work."
)
}
const prismaBin = which(process.cwd()).sync('prisma')
await new Promise((res, rej) => {
const process = spawn(
prismaBin,
['migrate', 'reset', '--force', '--skip-generate'],
{
stdio: 'ignore',
}
)
process.on('exit', (code) => (code === 0 ? res(0) : rej(code)))
})
global._blitz_prismaClient.$disconnect()
}
global._blitz_prismaClient = client
}
return global._blitz_prismaClient
},
}
)
}

View File

@@ -1,18 +1,11 @@
import {ZodError} from "zod"
import {ParserType} from "../server/resolver"
export const isServer = typeof window === "undefined"
export const isClient = typeof window !== "undefined"
export function clientDebug(...args: any) {
if (typeof window !== "undefined" && (window as any)["DEBUG_BLITZ"]) {
console.log("[BLITZ]", Date.now(), ...args)
}
}
import { ParserType } from '../types/index'
import { ZodError } from 'zod'
export function formatZodError(error: ZodError) {
if (!error || typeof error.format !== "function") {
throw new Error("The argument to formatZodError must be a zod error with error.format()")
if (!error || typeof error.format !== 'function') {
throw new Error(
'The argument to formatZodError must be a zod error with error.format()'
)
}
const errors = error.format()
@@ -23,7 +16,7 @@ export function recursiveFormatZodErrors(errors: any) {
let formattedErrors: Record<string, any> = {}
for (const key in errors) {
if (key === "_errors") {
if (key === '_errors') {
continue
}
@@ -65,11 +58,20 @@ const validateZodSchemaAsync = (schema: any) => async (values: any) => {
// type zodSchemaReturn = typeof validateZodSchemaAsync | typeof validateZodSchemaSync
// : (((values:any) => any) | ((values:any) => Promise<any>)) =>
export function validateZodSchema(schema: any, parserType: "sync"): (values: any) => any
export function validateZodSchema(schema: any, parserType: "async"): (values: any) => Promise<any>
export function validateZodSchema(
schema: any,
parserType: 'sync'
): (values: any) => any
export function validateZodSchema(
schema: any,
parserType: 'async'
): (values: any) => Promise<any>
export function validateZodSchema(schema: any): (values: any) => Promise<any>
export function validateZodSchema(schema: any, parserType: ParserType = "async") {
if (parserType === "sync") {
export function validateZodSchema(
schema: any,
parserType: ParserType = 'async'
) {
if (parserType === 'sync') {
return validateZodSchemaSync(schema)
} else {
return validateZodSchemaAsync(schema)

View File

@@ -405,6 +405,16 @@ export async function ncc_lodash_curry(task, opts) {
.target('compiled/lodash.curry')
}
// eslint-disable-next-line camelcase
externals['lodash.frompairs'] = 'next/dist/compiled/lodash.frompairs'
export async function ncc_lodash_frompairs(task, opts) {
await task
.source(
opts.src || relative(__dirname, require.resolve('lodash.frompairs'))
)
.ncc({ packageName: 'lodash.frompairs', externals })
.target('compiled/lodash.frompairs')
}
// eslint-disable-next-line camelcase
externals['lru-cache'] = 'next/dist/compiled/lru-cache'
export async function ncc_lru_cache(task, opts) {
await task
@@ -781,6 +791,7 @@ export async function ncc(task, opts) {
'ncc_jsonwebtoken',
'ncc_loader_utils',
'ncc_lodash_curry',
'ncc_lodash_frompairs',
'ncc_lru_cache',
'ncc_nanoid',
'ncc_neo_async',

View File

@@ -7,6 +7,9 @@
"esModuleInterop": true,
"moduleResolution": "node",
"useUnknownInCatchVariables": false,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"jsx": "react"
},
"exclude": ["dist", "./*.d.ts"]

View File

@@ -9,6 +9,10 @@ declare namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: 'development' | 'production' | 'test'
}
interface Global {
_blitz_prismaClient: any
}
}
declare module '*.module.css' {

View File

@@ -262,3 +262,5 @@ export type InferGetServerSidePropsType<T> = T extends GetServerSideProps<
) => Promise<GetServerSidePropsResult<infer P>>
? P
: never
export type ParserType = 'sync' | 'async'

View File

@@ -143,49 +143,13 @@ declare module 'next/dist/compiled/jsonwebtoken' {
import m from 'jsonwebtoken'
export = m
}
declare module 'next/dist/compiled/lodash.frompairs' {
import m from 'lodash.frompairs'
export = m
}
declare module 'next/dist/compiled/lodash.curry' {
// import m from 'lodash.curry'
// export = m
/*
* Blitz: inlining the types here because build was unable to pull types from lodash.curry
*/
export interface CurriedFunction1<T1, R> {
(): CurriedFunction1<T1, R>
(t1: T1): R
}
export interface CurriedFunction2<T1, T2, R> {
(): CurriedFunction2<T1, T2, R>
(t1: T1): CurriedFunction1<T2, R>
(t1: __, t2: T2): CurriedFunction1<T1, R>
(t1: T1, t2: T2): R
}
export interface CurriedFunction3<T1, T2, T3, R> {
(): CurriedFunction3<T1, T2, T3, R>
(t1: T1): CurriedFunction2<T2, T3, R>
(t1: __, t2: T2): CurriedFunction2<T1, T3, R>
(t1: T1, t2: T2): CurriedFunction1<T3, R>
(t1: __, t2: __, t3: T3): CurriedFunction2<T1, T2, R>
(t1: T1, t2: __, t3: T3): CurriedFunction1<T2, R>
(t1: __, t2: T2, t3: T3): CurriedFunction1<T1, R>
(t1: T1, t2: T2, t3: T3): R
}
interface Curry {
<T1, R>(func: (t1: T1) => R, arity?: number): CurriedFunction1<T1, R>
<T1, T2, R>(func: (t1: T1, t2: T2) => R, arity?: number): CurriedFunction2<
T1,
T2,
R
>
<T1, T2, T3, R>(
func: (t1: T1, t2: T2, t3: T3) => R,
arity?: number
): CurriedFunction3<T1, T2, T3, R>
(func: (...args: any[]) => any, arity?: number): (...args: any[]) => any
placeholder: __
}
const curry: Curry
export = curry
import m from 'lodash.curry'
export = m
}
declare module 'next/dist/compiled/lru-cache' {
import m from 'lru-cache'

View File

@@ -1,9 +1,9 @@
import {render, screen} from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import * as React from "react"
import type {FallbackProps} from "./error-boundary"
import {ErrorBoundary, useErrorHandler} from "./error-boundary"
import {cleanStack} from "./error-boundary.test"
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import type { ErrorFallbackProps } from 'next/stdlib'
import { ErrorBoundary, useErrorHandler } from 'next/stdlib'
import { cleanStack } from './error-boundary.unit.test'
afterEach(() => {
jest.resetAllMocks()
@@ -11,7 +11,7 @@ afterEach(() => {
})
beforeEach(() => {
jest.spyOn(console, "error").mockImplementation(() => {})
jest.spyOn(console, 'error').mockImplementation(() => {})
})
// afterEach(() => {
@@ -24,7 +24,7 @@ beforeEach(() => {
// }
// })
function ErrorFallback({error, resetErrorBoundary}: FallbackProps) {
function ErrorFallback({ error, resetErrorBoundary }: ErrorFallbackProps) {
return (
<div role="alert">
<p>Something went wrong:</p>
@@ -34,16 +34,16 @@ function ErrorFallback({error, resetErrorBoundary}: FallbackProps) {
)
}
const firstLine = (str: string) => str.split("\n")[0]
const firstLine = (str: string) => str.split('\n')[0]
test("handleError forwards along async errors", async () => {
test('handleError forwards along async errors', async () => {
function AsyncBomb() {
const [explode, setExplode] = React.useState(false)
const handleError = useErrorHandler()
React.useEffect(() => {
if (explode) {
setTimeout(() => {
handleError(new Error("💥 CABOOM 💥"))
handleError(new Error('💥 CABOOM 💥'))
})
}
})
@@ -52,17 +52,19 @@ test("handleError forwards along async errors", async () => {
render(
<ErrorBoundary FallbackComponent={ErrorFallback}>
<AsyncBomb />
</ErrorBoundary>,
</ErrorBoundary>
)
userEvent.click(screen.getByRole("button", {name: /bomb/i}))
userEvent.click(screen.getByRole('button', { name: /bomb/i }))
await screen.findByRole("alert")
await screen.findByRole('alert')
const consoleError = console.error as jest.Mock<void, unknown[]>
const [[actualError], [componentStack]] = consoleError.mock.calls
const firstLineOfError = firstLine(actualError as string)
expect(firstLineOfError).toMatchInlineSnapshot(`"Error: Uncaught [Error: 💥 CABOOM 💥]"`)
expect(firstLineOfError).toMatchInlineSnapshot(
`"Error: Uncaught [Error: 💥 CABOOM 💥]"`
)
expect(cleanStack(componentStack)).toMatchInlineSnapshot(`
"The above error occurred in the <AsyncBomb> component:
@@ -75,11 +77,11 @@ React will try to recreate this component tree from scratch using the error boun
consoleError.mockClear()
// can recover
userEvent.click(screen.getByRole("button", {name: /try again/i}))
userEvent.click(screen.getByRole('button', { name: /try again/i }))
expect(console.error).not.toHaveBeenCalled()
})
test("can pass an error to useErrorHandler", async () => {
test('can pass an error to useErrorHandler', async () => {
function AsyncBomb() {
const [error, setError] = React.useState<Error | null>(null)
const [explode, setExplode] = React.useState(false)
@@ -87,7 +89,7 @@ test("can pass an error to useErrorHandler", async () => {
React.useEffect(() => {
if (explode) {
setTimeout(() => {
setError(new Error("💥 CABOOM 💥"))
setError(new Error('💥 CABOOM 💥'))
})
}
})
@@ -96,17 +98,19 @@ test("can pass an error to useErrorHandler", async () => {
render(
<ErrorBoundary FallbackComponent={ErrorFallback}>
<AsyncBomb />
</ErrorBoundary>,
</ErrorBoundary>
)
userEvent.click(screen.getByRole("button", {name: /bomb/i}))
userEvent.click(screen.getByRole('button', { name: /bomb/i }))
await screen.findByRole("alert")
await screen.findByRole('alert')
const consoleError = console.error as jest.Mock<void, unknown[]>
const [[actualError], [componentStack]] = consoleError.mock.calls
const firstLineOfError = firstLine(actualError as string)
expect(firstLineOfError).toMatchInlineSnapshot(`"Error: Uncaught [Error: 💥 CABOOM 💥]"`)
expect(firstLineOfError).toMatchInlineSnapshot(
`"Error: Uncaught [Error: 💥 CABOOM 💥]"`
)
expect(cleanStack(componentStack)).toMatchInlineSnapshot(`
"The above error occurred in the <AsyncBomb> component:
@@ -119,6 +123,6 @@ React will try to recreate this component tree from scratch using the error boun
consoleError.mockClear()
// can recover
userEvent.click(screen.getByRole("button", {name: /try again/i}))
userEvent.click(screen.getByRole('button', { name: /try again/i }))
expect(console.error).not.toHaveBeenCalled()
})

View File

@@ -1,8 +1,8 @@
import {render, screen} from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import React from "react"
import type {FallbackProps} from "./error-boundary"
import {ErrorBoundary, withErrorBoundary} from "./error-boundary"
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import React from 'react'
import type { ErrorFallbackProps } from 'next/stdlib'
import { ErrorBoundary, withErrorBoundary } from 'next/stdlib'
afterEach(() => {
jest.resetAllMocks()
@@ -10,7 +10,7 @@ afterEach(() => {
})
beforeEach(() => {
jest.spyOn(console, "error").mockImplementation(() => {})
jest.spyOn(console, 'error').mockImplementation(() => {})
})
// afterEach(() => {
@@ -23,7 +23,7 @@ beforeEach(() => {
// }
// })
function ErrorFallback({error, resetErrorBoundary}: FallbackProps) {
function ErrorFallback({ error, resetErrorBoundary }: ErrorFallbackProps) {
return (
<div role="alert">
<p>Something went wrong:</p>
@@ -34,29 +34,29 @@ function ErrorFallback({error, resetErrorBoundary}: FallbackProps) {
}
function Bomb() {
throw new Error("💥 CABOOM 💥")
throw new Error('💥 CABOOM 💥')
// eslint-disable-next-line
return null
}
const firstLine = (str: string) => str.split("\n")[0]
const firstLine = (str: string) => str.split('\n')[0]
export const cleanStack = (stack: any): any => {
if (typeof stack === "string") {
return stack.replace(/\(.*\)/g, "")
if (typeof stack === 'string') {
return stack.replace(/\(.*\)/g, '')
}
if (typeof stack === "object" && stack.componentStack) {
if (typeof stack === 'object' && stack.componentStack) {
stack.componentStack = cleanStack(stack.componentStack)
return stack
}
return stack
}
test("standard use-case", () => {
test('standard use-case', () => {
const consoleError = console.error as jest.Mock<void, unknown[]>
function App() {
const [username, setUsername] = React.useState("")
const [username, setUsername] = React.useState('')
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setUsername(e.target.value)
}
@@ -66,10 +66,10 @@ test("standard use-case", () => {
<label htmlFor="username">Username</label>
<input type="text" id="username" onChange={handleChange} />
</div>
<div>{username === "fail" ? "Oh no" : "things are good"}</div>
<div>{username === 'fail' ? 'Oh no' : 'things are good'}</div>
<div>
<ErrorBoundary FallbackComponent={ErrorFallback}>
{username === "fail" ? <Bomb /> : 'type "fail"'}
{username === 'fail' ? <Bomb /> : 'type "fail"'}
</ErrorBoundary>
</div>
</div>
@@ -78,11 +78,11 @@ test("standard use-case", () => {
render(<App />)
userEvent.type(screen.getByRole("textbox", {name: /username/i}), "fail")
userEvent.type(screen.getByRole('textbox', { name: /username/i }), 'fail')
const [[actualError], [componentStack]] = consoleError.mock.calls
expect(firstLine(actualError as string)).toMatchInlineSnapshot(
`"Error: Uncaught [Error: 💥 CABOOM 💥]"`,
`"Error: Uncaught [Error: 💥 CABOOM 💥]"`
)
expect(cleanStack(componentStack)).toMatchInlineSnapshot(`
"The above error occurred in the <Bomb> component:
@@ -98,7 +98,7 @@ React will try to recreate this component tree from scratch using the error boun
expect(consoleError).toHaveBeenCalledTimes(2)
consoleError.mockClear()
expect(screen.getByRole("alert")).toMatchInlineSnapshot(`
expect(screen.getByRole('alert')).toMatchInlineSnapshot(`
<div
role="alert"
>
@@ -115,22 +115,22 @@ React will try to recreate this component tree from scratch using the error boun
`)
// can recover from errors when the component is rerendered and reset is clicked
userEvent.type(screen.getByRole("textbox", {name: /username/i}), "-not")
userEvent.click(screen.getByRole("button", {name: /try again/i}))
expect(screen.queryByRole("alert")).not.toBeInTheDocument()
userEvent.type(screen.getByRole('textbox', { name: /username/i }), '-not')
userEvent.click(screen.getByRole('button', { name: /try again/i }))
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
})
test("fallbackRender prop", () => {
test('fallbackRender prop', () => {
const consoleError = console.error as jest.Mock<void, unknown[]>
const workingMessage = "Phew, we are safe!"
const workingMessage = 'Phew, we are safe!'
function App() {
const [explode, setExplode] = React.useState(true)
return (
<div>
<ErrorBoundary
fallbackRender={({resetErrorBoundary}) => (
fallbackRender={({ resetErrorBoundary }) => (
<button
onClick={() => {
setExplode(false)
@@ -153,18 +153,18 @@ test("fallbackRender prop", () => {
// the render prop API allows a single action to reset the app state
// as well as reset the ErrorBoundary state
userEvent.click(screen.getByRole("button", {name: /try again/i}))
userEvent.click(screen.getByRole('button', { name: /try again/i }))
expect(screen.getByText(workingMessage)).toBeInTheDocument()
})
test("simple fallback is supported", () => {
test('simple fallback is supported', () => {
const consoleError = console.error as jest.Mock<void, unknown[]>
render(
<ErrorBoundary fallback={<div>Oh no</div>}>
<Bomb />
<span>child</span>
</ErrorBoundary>,
</ErrorBoundary>
)
expect(consoleError).toHaveBeenCalledTimes(2)
consoleError.mockClear()
@@ -172,21 +172,23 @@ test("simple fallback is supported", () => {
expect(screen.queryByText(/child/i)).not.toBeInTheDocument()
})
test("withErrorBoundary HOC", () => {
test('withErrorBoundary HOC', () => {
const consoleError = console.error as jest.Mock<void, unknown[]>
const onErrorHandler = jest.fn()
const Boundary = withErrorBoundary(
() => {
throw new Error("💥 CABOOM 💥")
throw new Error('💥 CABOOM 💥')
},
{FallbackComponent: ErrorFallback, onError: onErrorHandler},
{ FallbackComponent: ErrorFallback, onError: onErrorHandler }
)
render(<Boundary />)
const [[actualError], [componentStack]] = consoleError.mock.calls
const firstLineOfError = firstLine(actualError as string)
expect(firstLineOfError).toMatchInlineSnapshot(`"Error: Uncaught [Error: 💥 CABOOM 💥]"`)
expect(firstLineOfError).toMatchInlineSnapshot(
`"Error: Uncaught [Error: 💥 CABOOM 💥]"`
)
expect(cleanStack(componentStack)).toMatchInlineSnapshot(`
"The above error occurred in one of your React components:
@@ -199,7 +201,9 @@ React will try to recreate this component tree from scratch using the error boun
expect(consoleError).toHaveBeenCalledTimes(2)
consoleError.mockClear()
const [error, onErrorComponentStack] = (onErrorHandler.mock.calls as [[Error, string]])[0]
const [error, onErrorComponentStack] = (onErrorHandler.mock.calls as [
[Error, string]
])[0]
expect(error.message).toMatchInlineSnapshot(`"💥 CABOOM 💥"`)
expect(cleanStack(onErrorComponentStack)).toMatchInlineSnapshot(`
Object {
@@ -212,10 +216,10 @@ Object {
expect(onErrorHandler).toHaveBeenCalledTimes(1)
})
test("supported but undocumented reset method", () => {
test('supported but undocumented reset method', () => {
const consoleError = console.error as jest.Mock<void, unknown[]>
const children = "Boundry children"
const children = 'Boundry children'
function App() {
const errorBoundaryRef = React.useRef<ErrorBoundary | null>(null)
const [explode, setExplode] = React.useState(false)
@@ -237,18 +241,18 @@ test("supported but undocumented reset method", () => {
)
}
render(<App />)
userEvent.click(screen.getByText("explode"))
userEvent.click(screen.getByText('explode'))
expect(screen.queryByText(children)).not.toBeInTheDocument()
expect(consoleError).toHaveBeenCalledTimes(2)
consoleError.mockClear()
userEvent.click(screen.getByText("recover"))
userEvent.click(screen.getByText('recover'))
expect(screen.getByText(children)).toBeInTheDocument()
expect(consoleError).toHaveBeenCalledTimes(0)
})
test("requires either a fallback, fallbackRender, or FallbackComponent", () => {
test('requires either a fallback, fallbackRender, or FallbackComponent', () => {
const consoleError = console.error as jest.Mock<void, unknown[]>
expect(() =>
@@ -256,21 +260,21 @@ test("requires either a fallback, fallbackRender, or FallbackComponent", () => {
// @ts-expect-error we're testing the runtime check of missing props here
<ErrorBoundary>
<Bomb />
</ErrorBoundary>,
),
</ErrorBoundary>
)
).toThrowErrorMatchingInlineSnapshot(
`"<ErrorBoundary> requires either a fallback, fallbackRender, or FallbackComponent prop"`,
`"<ErrorBoundary> requires either a fallback, fallbackRender, or FallbackComponent prop"`
)
consoleError.mockClear()
})
// eslint-disable-next-line max-statements
test("supports automatic reset of error boundary when resetKeys change", () => {
test('supports automatic reset of error boundary when resetKeys change', () => {
const consoleError = console.error as jest.Mock<void, unknown[]>
const handleReset = jest.fn()
const TRY_AGAIN_ARG1 = "TRY_AGAIN_ARG1"
const TRY_AGAIN_ARG2 = "TRY_AGAIN_ARG2"
const TRY_AGAIN_ARG1 = 'TRY_AGAIN_ARG1'
const TRY_AGAIN_ARG2 = 'TRY_AGAIN_ARG2'
const handleResetKeysChange = jest.fn()
function App() {
const [explode, setExplode] = React.useState(false)
@@ -279,12 +283,18 @@ test("supports automatic reset of error boundary when resetKeys change", () => {
<div>
<button onClick={() => setExplode((e) => !e)}>toggle explode</button>
<ErrorBoundary
fallbackRender={({resetErrorBoundary}) => (
fallbackRender={({ resetErrorBoundary }) => (
<div role="alert">
<button onClick={() => resetErrorBoundary(TRY_AGAIN_ARG1, TRY_AGAIN_ARG2)}>
<button
onClick={() =>
resetErrorBoundary(TRY_AGAIN_ARG1, TRY_AGAIN_ARG2)
}
>
Try again
</button>
<button onClick={() => setExtra((e) => !e)}>toggle extra resetKey</button>
<button onClick={() => setExtra((e) => !e)}>
toggle extra resetKey
</button>
</div>
)}
onReset={(...args) => {
@@ -302,14 +312,14 @@ test("supports automatic reset of error boundary when resetKeys change", () => {
render(<App />)
// blow it up
userEvent.click(screen.getByText("toggle explode"))
expect(screen.getByRole("alert")).toBeInTheDocument()
userEvent.click(screen.getByText('toggle explode'))
expect(screen.getByRole('alert')).toBeInTheDocument()
expect(consoleError).toHaveBeenCalledTimes(2)
consoleError.mockClear()
// recover via try again button
userEvent.click(screen.getByText(/try again/i))
expect(screen.queryByRole("alert")).not.toBeInTheDocument()
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
expect(consoleError).not.toHaveBeenCalled()
expect(handleReset).toHaveBeenCalledWith(TRY_AGAIN_ARG1, TRY_AGAIN_ARG2)
expect(handleReset).toHaveBeenCalledTimes(1)
@@ -317,59 +327,62 @@ test("supports automatic reset of error boundary when resetKeys change", () => {
expect(handleResetKeysChange).not.toHaveBeenCalled()
// blow it up again
userEvent.click(screen.getByText("toggle explode"))
expect(screen.getByRole("alert")).toBeInTheDocument()
userEvent.click(screen.getByText('toggle explode'))
expect(screen.getByRole('alert')).toBeInTheDocument()
expect(consoleError).toHaveBeenCalledTimes(2)
consoleError.mockClear()
// recover via resetKeys change
userEvent.click(screen.getByText("toggle explode"))
userEvent.click(screen.getByText('toggle explode'))
expect(handleResetKeysChange).toHaveBeenCalledWith([true], [false])
expect(handleResetKeysChange).toHaveBeenCalledTimes(1)
handleResetKeysChange.mockClear()
expect(handleReset).not.toHaveBeenCalled()
expect(screen.queryByRole("alert")).not.toBeInTheDocument()
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
expect(consoleError).not.toHaveBeenCalled()
// blow it up again
userEvent.click(screen.getByText("toggle explode"))
expect(screen.getByRole("alert")).toBeInTheDocument()
userEvent.click(screen.getByText('toggle explode'))
expect(screen.getByRole('alert')).toBeInTheDocument()
expect(consoleError).toHaveBeenCalledTimes(2)
consoleError.mockClear()
// toggles adding an extra resetKey to the array
// expect error to re-render
userEvent.click(screen.getByText("toggle extra resetKey"))
userEvent.click(screen.getByText('toggle extra resetKey'))
expect(handleResetKeysChange).toHaveBeenCalledTimes(1)
expect(handleResetKeysChange).toHaveBeenCalledWith([true], [true, true])
handleResetKeysChange.mockClear()
expect(screen.getByRole("alert")).toBeInTheDocument()
expect(screen.getByRole('alert')).toBeInTheDocument()
expect(consoleError).toHaveBeenCalledTimes(2)
consoleError.mockClear()
// toggle explode back to false
// expect error to re-render again
userEvent.click(screen.getByText("toggle explode"))
userEvent.click(screen.getByText('toggle explode'))
expect(handleReset).not.toHaveBeenCalled()
expect(handleResetKeysChange).toHaveBeenCalledTimes(1)
expect(handleResetKeysChange).toHaveBeenCalledWith([true, true], [false, true])
expect(screen.getByRole("alert")).toBeInTheDocument()
expect(handleResetKeysChange).toHaveBeenCalledWith(
[true, true],
[false, true]
)
expect(screen.getByRole('alert')).toBeInTheDocument()
handleResetKeysChange.mockClear()
expect(consoleError).toHaveBeenCalledTimes(2)
consoleError.mockClear()
// toggle extra resetKey
// expect error to be reset
userEvent.click(screen.getByText("toggle extra resetKey"))
userEvent.click(screen.getByText('toggle extra resetKey'))
expect(handleReset).not.toHaveBeenCalled()
expect(handleResetKeysChange).toHaveBeenCalledTimes(1)
expect(handleResetKeysChange).toHaveBeenCalledWith([false, true], [false])
handleResetKeysChange.mockClear()
expect(screen.queryByRole("alert")).not.toBeInTheDocument()
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
expect(consoleError).not.toHaveBeenCalled()
})
test("supports reset via resetKeys right after error is triggered on component mount", () => {
test('supports reset via resetKeys right after error is triggered on component mount', () => {
const consoleError = console.error as jest.Mock<void, unknown[]>
const handleResetKeysChange = jest.fn()
function App() {
@@ -394,43 +407,45 @@ test("supports reset via resetKeys right after error is triggered on component m
render(<App />)
// it blows up on render
expect(screen.getByRole("alert")).toBeInTheDocument()
expect(screen.getByRole('alert')).toBeInTheDocument()
expect(consoleError).toHaveBeenCalledTimes(2)
consoleError.mockClear()
// recover via "toggle explode" button
userEvent.click(screen.getByText("toggle explode"))
expect(screen.queryByRole("alert")).not.toBeInTheDocument()
userEvent.click(screen.getByText('toggle explode'))
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
expect(consoleError).not.toHaveBeenCalled()
expect(handleResetKeysChange).toHaveBeenCalledWith([true], [false])
expect(handleResetKeysChange).toHaveBeenCalledTimes(1)
})
test("should support not only function as FallbackComponent", () => {
test('should support not only function as FallbackComponent', () => {
const consoleError = console.error as jest.Mock<void, unknown[]>
const FancyFallback = React.forwardRef(({error}: FallbackProps) => (
const FancyFallback = React.forwardRef(({ error }: FallbackProps) => (
<div>
<p>Everything is broken. Try again</p>
<pre>{error.message}</pre>
</div>
))
FancyFallback.displayName = "FancyFallback"
FancyFallback.displayName = 'FancyFallback'
expect(() =>
render(
<ErrorBoundary FallbackComponent={FancyFallback}>
<Bomb />
</ErrorBoundary>,
),
</ErrorBoundary>
)
).not.toThrow()
expect(screen.getByText("Everything is broken. Try again")).toBeInTheDocument()
expect(
screen.getByText('Everything is broken. Try again')
).toBeInTheDocument()
consoleError.mockClear()
})
test("should throw error if FallbackComponent is not valid", () => {
test('should throw error if FallbackComponent is not valid', () => {
const consoleError = console.error as jest.Mock<void, unknown[]>
expect(() =>
@@ -438,8 +453,8 @@ test("should throw error if FallbackComponent is not valid", () => {
// @ts-expect-error we're testing the error case
<ErrorBoundary FallbackComponent={{}}>
<Bomb />
</ErrorBoundary>,
),
</ErrorBoundary>
)
).toThrowError(/Element type is invalid/i)
consoleError.mockClear()

View File

@@ -0,0 +1,62 @@
import { ParserType, Ctx } from 'next/types'
import { z } from 'zod'
import { resolver } from 'next/stdlib-server'
describe('resolver', () => {
it('should typecheck and pass along value', async () => {
await resolverTest({})
})
it('should typecheck and pass along value if sync resolver is specified', async () => {
await resolverTest({ type: 'sync' })
})
it('should typecheck and pass along value if async resolver is specified', async () => {
await resolverTest({ type: 'async' })
})
})
const syncResolver = resolver.pipe(
resolver.zod(
z.object({
email: z.string().email(),
}),
'sync'
),
resolver.authorize({}),
(input) => {
return input.email
}
)
const asyncResolver = resolver.pipe(
resolver.zod(
z.object({
email: z.string().email(),
}),
'async'
),
resolver.authorize({}),
(input) => {
return input.email
}
)
const resolverTest = async ({ type }: { type?: ParserType }) => {
const resolver1 = type === 'sync' ? syncResolver : asyncResolver
const result1 = await resolver1(
{ email: 'test@example.com' },
{ session: { $authorize: () => undefined } as Ctx }
)
expect(result1).toBe('test@example.com')
const resolver2 = resolver.pipe(
/*resolver.authorize(), */ (input: { email: string }) => {
return input.email
}
)
const result2 = await resolver2(
{ email: 'test@example.com' },
{ session: { $authorize: () => undefined } as Ctx }
)
expect(result2).toBe('test@example.com')
}

View File

@@ -0,0 +1,257 @@
import { useParam, useParams, useRouterQuery } from 'next/router'
import { renderHook } from '../blitz-test-utils'
import { extractRouterParams } from 'next/dist/shared/lib/router/router'
describe('useRouterQuery', () => {
it('returns proper values', () => {
const { result } = renderHook(() => useRouterQuery(), {
router: { asPath: '/?foo=foo&num=0&bool=true&float=1.23&empty' },
})
expect(result.current).toEqual({
foo: 'foo',
num: '0',
bool: 'true',
float: '1.23',
empty: '',
})
})
it('decode correctly', () => {
const { result } = renderHook(() => useRouterQuery(), {
router: {
asPath:
'/?encoded=D%C3%A9j%C3%A0%20vu&spaces=Hello+World&both=Hola%2C+Mundo%21',
},
})
expect(result.current).toEqual({
encoded: 'Déjà vu',
spaces: 'Hello World',
both: 'Hola, Mundo!',
})
})
})
describe('extractRouterParams', () => {
it('returns proper params', () => {
const routerQuery = {
id: '1',
cat: 'category',
slug: ['example', 'multiple', 'slugs'],
empty: '',
queryArray: ['1', '123', ''],
}
const query = {
cat: 'somethingelse',
slug: ['query-slug'],
queryArray: ['1', '123', ''],
onlyInQuery: 'onlyInQuery',
}
const params = extractRouterParams(routerQuery, query)
expect(params).toEqual({
id: '1',
cat: 'category',
slug: ['example', 'multiple', 'slugs'],
empty: '',
})
})
})
describe('useParams', () => {
it('works without parameter', () => {
// This is the router query object which includes route params
const query = {
id: '1',
cat: 'category',
slug: ['example', 'multiple', 'slugs'],
empty: '',
}
const { result } = renderHook(() => useParams(), { router: { query } })
expect(result.current).toEqual({
id: '1',
cat: 'category',
slug: ['example', 'multiple', 'slugs'],
empty: '',
})
})
it('works with string', () => {
// This is the router query object which includes route params
const query = {
id: '1',
cat: 'category',
slug: ['example', 'multiple', 'slugs'],
empty: '',
}
const { result } = renderHook(() => useParams('string'), {
router: { query },
})
expect(result.current).toEqual({
id: '1',
cat: 'category',
empty: '',
})
})
it('works with number', () => {
// This is the router query object which includes route params
const query = {
id: '1',
cat: 'category',
slug: ['example', 'multiple', 'slugs'],
empty: '',
}
const { result } = renderHook(() => useParams('number'), {
router: { query },
})
expect(result.current).toEqual({
id: 1,
cat: undefined,
slug: undefined,
})
})
it('works with array', () => {
// This is the router query object which includes route params
const query = {
id: '1',
cat: 'category',
slug: ['example', 'multiple', 'slugs'],
empty: '',
}
const { result } = renderHook(() => useParams('array'), {
router: { query },
})
expect(result.current).toEqual({
id: ['1'],
cat: ['category'],
slug: ['example', 'multiple', 'slugs'],
empty: [''],
})
})
})
describe('useParam', () => {
it('works without parameter', () => {
// This is the router query object which includes route params
const query = {
id: '1',
cat: 'category',
slug: ['example', 'multiple', 'slugs'],
empty: '',
}
let { result } = renderHook(() => useParam('id'), { router: { query } })
expect(result.current).toEqual('1')
;({ result } = renderHook(() => useParam('cat'), { router: { query } }))
expect(result.current).toEqual('category')
;({ result } = renderHook(() => useParam('slug'), { router: { query } }))
expect(result.current).toEqual(['example', 'multiple', 'slugs'])
;({ result } = renderHook(() => useParam('empty'), { router: { query } }))
expect(result.current).toEqual('')
;({ result } = renderHook(() => useParam('doesnt-exist'), {
router: { query },
}))
expect(result.current).toBeUndefined()
})
it('works with string', () => {
// This is the router query object which includes route params
const query = {
id: '1',
cat: 'category',
slug: ['example', 'multiple', 'slugs'],
empty: '',
}
let { result } = renderHook(() => useParam('id', 'string'), {
router: { query },
})
expect(result.current).toEqual('1')
;({ result } = renderHook(() => useParam('cat', 'string'), {
router: { query },
}))
expect(result.current).toEqual('category')
;({ result } = renderHook(() => useParam('slug', 'string'), {
router: { query },
}))
expect(result.current).toEqual(undefined)
;({ result } = renderHook(() => useParam('empty', 'string'), {
router: { query },
}))
expect(result.current).toEqual('')
;({ result } = renderHook(() => useParam('doesnt-exist', 'string'), {
router: { query },
}))
expect(result.current).toBeUndefined()
})
it('works with number', () => {
// This is the router query object which includes route params
const query = {
id: '1',
cat: 'category',
slug: ['example', 'multiple', 'slugs'],
empty: '',
}
let { result } = renderHook(() => useParam('id', 'number'), {
router: { query },
})
expect(result.current).toEqual(1)
;({ result } = renderHook(() => useParam('cat', 'number'), {
router: { query },
}))
expect(result.current).toBeUndefined()
;({ result } = renderHook(() => useParam('slug', 'number'), {
router: { query },
}))
expect(result.current).toBeUndefined()
;({ result } = renderHook(() => useParam('empty', 'number'), {
router: { query },
}))
expect(result.current).toBeUndefined()
;({ result } = renderHook(() => useParam('doesnt-exist', 'number'), {
router: { query },
}))
expect(result.current).toBeUndefined()
})
it('works with array', () => {
// This is the router query object which includes route params
const query = {
id: '1',
cat: 'category',
slug: ['example', 'multiple', 'slugs'],
empty: '',
}
let { result } = renderHook(() => useParam('id', 'array'), {
router: { query },
})
expect(result.current).toEqual(['1'])
;({ result } = renderHook(() => useParam('cat', 'array'), {
router: { query },
}))
expect(result.current).toEqual(['category'])
;({ result } = renderHook(() => useParam('slug', 'array'), {
router: { query },
}))
expect(result.current).toEqual(['example', 'multiple', 'slugs'])
;({ result } = renderHook(() => useParam('empty', 'array'), {
router: { query },
}))
expect(result.current).toEqual([''])
;({ result } = renderHook(() => useParam('doesnt-exist', 'array'), {
router: { query },
}))
expect(result.current).toBeUndefined()
})
})

View File

@@ -0,0 +1,105 @@
import { z } from 'zod'
import { formatZodError, validateZodSchema } from 'next/stdlib'
const validateSchema = (schema: any, input: any) => {
const result = schema.safeParse(input)
if (result.success) throw new Error('Schema should not return success')
return result
}
const Schema = z.object({
test: z.string(),
})
describe('formatZodError', () => {
it('formats the zod error', () => {
expect(formatZodError(validateSchema(Schema, {}).error)).toEqual({
test: 'Required',
})
})
it('formats the nested zod error', () => {
const NestedSchema = z.object({
test: z.string(),
nested: z.object({
foo: z.string(),
test: z.string(),
}),
})
const result = validateSchema(NestedSchema, {
test: 'yo',
nested: { foo: 'yo' },
})
expect(formatZodError(result.error)).toEqual({
nested: { test: 'Required' },
})
})
it('formats 2 levels nested zod error', () => {
const DoubleNestedSchema = z.object({
test: z.string(),
nested: z.object({
test: z.string(),
doubleNested: z.object({
test: z.string(),
}),
}),
})
expect(
formatZodError(
validateSchema(DoubleNestedSchema, {
nested: { doubleNested: {} },
}).error
)
).toEqual({
test: 'Required',
nested: { test: 'Required', doubleNested: { test: 'Required' } },
})
})
it('formats arrays', () => {
const NestedSchema = z.object({
students: z.array(
z.object({
name: z.string(),
})
),
data: z.object({
1: z.literal(true),
}),
})
const result = validateSchema(NestedSchema, {
students: [{ name: 'hi' }, { wat: true }, { name: true }],
data: {},
})
expect(formatZodError(result.error)).toEqual({
students: [
undefined,
{ name: 'Required' },
{ name: 'Expected string, received boolean' },
],
data: [undefined, 'Expected true, received undefined'],
})
})
})
describe('validateZodSchema', () => {
it('passes validation', async () => {
expect(await validateZodSchema(Schema)({ test: 'test' })).toEqual({})
})
it('fails validation', async () => {
expect(await validateZodSchema(Schema)({})).toEqual({ test: 'Required' })
})
it('passes validation if synchronous', () => {
expect(validateZodSchema(Schema, 'sync')({ test: 'test' })).toEqual({})
})
it('fails validation if synchronous', () => {
expect(validateZodSchema(Schema, 'sync')({})).toEqual({ test: 'Required' })
})
})

View File

@@ -51,7 +51,7 @@
"reset": "rimraf node_modules && git clean -xfd packages && git clean -xfd test && git clean -xfd nextjs && yarn",
"publish-prep": "yarn && yarn build",
"prepack": "node scripts/prepack.js",
"postpublish": "rimraf packages/blitz/README.md && git checkout packages/core/package.json && git checkout nextjs/packages/next/package.json",
"postpublish": "rimraf packages/blitz/README.md && git checkout packages/blitz/package.json && git checkout nextjs/packages/next/package.json",
"publish-local": "yarn workspaces run yalc publish",
"publish-canary": "yarn run publish-prep && lerna publish --no-private --force-publish --preid canary --pre-dist-tag canary && manypkg fix && git add . && git commit -m 'bump recipe/example versions (ignore)' --no-verify && git push",
"publish-latest": "yarn run publish-prep && lerna publish --no-private --force-publish && manypkg fix && git add . && git commit -m 'bump recipe/example versions (ignore)' --no-verify && git push",
@@ -109,7 +109,6 @@
"@types/ink-spinner": "3.0.0",
"@types/jest": "26.0.20",
"@types/jsonwebtoken": "8.5.0",
"@types/lodash": "4.14.149",
"@types/lowdb": "1.0.9",
"@types/mem-fs": "1.1.2",
"@types/mem-fs-editor": "7.0.0",

View File

@@ -12,7 +12,7 @@ function AddBlitzAppRoot(): PluginObj {
return;
}
wrapExportDefaultDeclaration(path, 'withBlitzAppRoot', '@blitzjs/core');
wrapExportDefaultDeclaration(path, 'withBlitzAppRoot', 'next/stdlib');
},
},
};

View File

@@ -5,7 +5,7 @@ import { BabelType } from 'babel-plugin-tester';
* https://astexplorer.net/#/gist/dd0cdbd56a701d8c9e078d20505b3980/latest
*/
const defaultImportSource = '@blitzjs/core';
const defaultImportSource = 'next/stdlib';
const specialImports: Record<string, string> = {
Link: 'next/link',
@@ -18,12 +18,20 @@ const specialImports: Record<string, string> = {
Main: 'next/document',
BlitzScript: 'next/document',
AuthenticationError: 'next/stdlib',
AuthorizationError: 'next/stdlib',
CSRFTokenMismatchError: 'next/stdlib',
NotFoundError: 'next/stdlib',
PaginationArgumentError: 'next/stdlib',
RedirectError: 'next/stdlib',
// AuthenticationError: 'next/stdlib',
// AuthorizationError: 'next/stdlib',
// CSRFTokenMismatchError: 'next/stdlib',
// NotFoundError: 'next/stdlib',
// PaginationArgumentError: 'next/stdlib',
// RedirectError: 'next/stdlib',
// formatZodError: 'next/stdlib',
// recursiveFormatZodErrors: 'next/stdlib',
// validateZodSchema: 'next/stdlib',
// enhancePrisma: 'next/stdlib',
// ErrorBoundary: 'next/stdlib',
// withErrorBoundary: 'next/stdlib',
// useErrorHandler: 'next/stdlib',
// withBlitzAppRoot: 'next/stdlib',
paginate: 'next/stdlib-server',
isLocalhost: 'next/stdlib-server',
@@ -36,6 +44,7 @@ const specialImports: Record<string, string> = {
SecurePassword: 'next/stdlib-server',
hash256: 'next/stdlib-server',
generateToken: 'next/stdlib-server',
resolver: 'next/stdlib-server',
BlitzProvider: 'next/data-client',
getAntiCSRFToken: 'next/data-client',
@@ -55,17 +64,17 @@ const specialImports: Record<string, string> = {
dehydrate: 'next/data-client',
invoke: 'next/data-client',
Head: '@blitzjs/core/head',
Head: 'next/head',
App: '@blitzjs/core/app',
App: 'next/app',
dynamic: '@blitzjs/core/dynamic',
noSSR: '@blitzjs/core/dynamic',
dynamic: 'next/dynamic',
noSSR: 'next/dynamic',
getConfig: '@blitzjs/core/config',
setConfig: '@blitzjs/core/config',
getConfig: 'next/config',
setConfig: 'next/config',
resolver: '@blitzjs/core/server',
ErrorComponent: 'next/error',
};
function RewriteImports(babel: BabelType): PluginObj {

View File

@@ -54,7 +54,6 @@
"@blitzjs/babel-preset": "0.41.2-canary.1",
"@blitzjs/cli": "0.41.2-canary.1",
"@blitzjs/config": "0.41.2-canary.1",
"@blitzjs/core": "0.41.2-canary.1",
"@blitzjs/display": "0.41.2-canary.1",
"@blitzjs/generator": "0.41.2-canary.1",
"@blitzjs/server": "0.41.2-canary.1",
@@ -70,6 +69,7 @@
"jest": "^26.6.3",
"jest-watch-typeahead": "^0.6.1",
"minimist": "1.2.5",
"next": "0.41.2-canary.1",
"os-name": "^4.0.0",
"pkg-dir": "^5.0.0",
"react-test-renderer": "17.0.1",

View File

@@ -85,7 +85,7 @@ function codegen() {
// Sometimes with npm the next package is missing because of how
// we use the `npm:@blitzjs/next` syntax to install the fork at node_modules/next
debug("Missing next package, manually installing...")
const corePkg = require("@blitzjs/core/package.json")
const corePkg = require("../package.json")
await run("npm", ["install", "--no-save", `next@${corePkg.dependencies.next}`], root, [
"ignore",
])

View File

@@ -1,9 +1,4 @@
export * from "@blitzjs/core/app"
export * from "@blitzjs/core/config"
export * from "@blitzjs/core/dynamic"
export * from "@blitzjs/core/head"
export * from "@blitzjs/core"
export * from "@blitzjs/core/server"
export type {BlitzConfig} from "@blitzjs/config"
/*
* IF YOU CHANGE THE BELOW EXPORTS
@@ -14,6 +9,13 @@ export {default as Image} from "next/image"
export type {ImageProps, ImageLoader, ImageLoaderProps} from "next/image"
export * from "next/link"
export * from "next/app"
export * from "next/config"
export * from "next/dynamic"
export * from "next/head"
export {ErrorComponent} from "next/error"
export type {ErrorProps} from "next/error"
export {Document, DocumentHead, Html, Main, BlitzScript} from "next/document"
export type {DocumentProps, DocumentContext, DocumentInitialProps} from "next/document"

View File

@@ -1,16 +0,0 @@
module.exports = {
extends: ["../../.eslintrc.js"],
plugins: ["es5", "es"],
rules: {
"es/no-object-fromentries": "error",
"es5/no-generators": "error",
"es5/no-typeof-symbol": "error",
"es5/no-es6-methods": "error",
"es5/no-es6-static-methods": [
"error",
{
exceptMethods: ["Object.assign"],
},
],
},
}

View File

@@ -1,8 +0,0 @@
*.log
.DS_Store
node_modules
.rts2_cache_cjs
.rts2_cache_esm
.rts2_cache_umd
.rts2_cache_system
dist

View File

@@ -1,39 +0,0 @@
# `core`
This package contains the application-facing offerings of BlitzJS.
Some of the fullstack features that are available include:
- Authentication Utilities
- React Hooks
- Session Management
- Wrappers for the data-layer communications (RPC)
## Usage
### Fetch data from a query
```js
import {useQuery} from "blitz"
import getUsers from "app/users/queries/getUsers"
const Users = () => {
const [users] = useQuery(getUsers, {})
return <pre style={{maxWidth: "30rem"}}>{JSON.stringify(users, null, 2)}</pre>
}
```
### Session Context
```ts
import {Ctx} from "blitz"
export default async function trackView(_ = null, {session}: Ctx) {
const currentViews = session.publicData.views || 0
await session.setPublicData({views: currentViews + 1})
await session.setPrivateData({views: currentViews + 1})
return
}
```

View File

@@ -1,4 +0,0 @@
{
"main": "dist/blitzjs-core-app.cjs.js",
"module": "dist/blitzjs-core-app.esm.js"
}

View File

@@ -1,5 +0,0 @@
{
"main": "dist/blitzjs-core-config.cjs.js",
"module": "dist/blitzjs-core-config.esm.js",
"types": "dist/blitzjs-core-config.cjs.d.ts"
}

View File

@@ -1,5 +0,0 @@
{
"main": "dist/blitzjs-core-dynamic.cjs.js",
"module": "dist/blitzjs-core-dynamic.esm.js",
"types": "dist/blitzjs-core-dynamic.cjs.d.ts"
}

View File

@@ -1,5 +0,0 @@
{
"main": "dist/blitzjs-core-head.cjs.js",
"module": "dist/blitzjs-core-head.esm.js",
"types": "dist/blitzjs-core-head.cjs.d.ts"
}

View File

@@ -1,4 +0,0 @@
module.exports = {
preset: "../../jest-unit.config.js",
testEnvironment: "jest-environment-jsdom",
}

View File

@@ -1,2 +0,0 @@
require("@testing-library/jest-dom")
process.env.BLITZ_TEST_ENVIRONMENT = true

View File

@@ -1,53 +0,0 @@
{
"name": "@blitzjs/core",
"description": "Blitz.js core functionality",
"version": "0.41.2-canary.1",
"license": "MIT",
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
},
"preconstruct": {
"entrypoints": [
"app.ts",
"config.ts",
"dynamic.ts",
"head.ts",
"index.ts",
"server/index.ts"
]
},
"main": "dist/blitzjs-core.cjs.js",
"module": "dist/blitzjs-core.esm.js",
"types": "dist/blitzjs-core.cjs.d.ts",
"files": [
"dist",
"app",
"config",
"dynamic",
"head",
"server"
],
"dependencies": {
"@blitzjs/config": "0.41.2-canary.1",
"@blitzjs/display": "0.41.2-canary.1",
"chalk": "^4.1.0",
"cross-spawn": "7.0.3",
"htmlescape": "^1.1.1",
"lodash.frompairs": "4.0.1",
"next": "0.41.2-canary.1",
"npm-which": "^3.0.1",
"superjson": "1.7.2"
},
"devDependencies": {
"react": "0.0.0-experimental-6a589ad71",
"zod": "3.8.1"
},
"repository": "https://github.com/blitz-js/blitz",
"author": {
"name": "Brandon Bayer",
"email": "b@bayer.ws",
"url": "https://twitter.com/flybayer"
},
"gitHead": "d3b9fce0bdd251c2b1890793b0aa1cd77c1c0922"
}

View File

@@ -1,5 +0,0 @@
{
"main": "dist/blitzjs-core-server.cjs.js",
"module": "dist/blitzjs-core-server.esm.js",
"types": "dist/blitzjs-core-server.cjs.d.ts"
}

View File

@@ -1,6 +0,0 @@
/*
* IF YOU CHANGE THIS FILE
* You also need to update the rewrite map in
* packages/babel-preset/src/rewrite-imports.ts
*/
export {default as App} from "next/app"

View File

@@ -1,6 +0,0 @@
/*
* IF YOU CHANGE THIS FILE
* You also need to update the rewrite map in
* packages/babel-preset/src/rewrite-imports.ts
*/
export {default as getConfig, setConfig} from "next/config"

View File

@@ -1,6 +0,0 @@
/*
* IF YOU CHANGE THIS FILE
* You also need to update the rewrite map in
* packages/babel-preset/src/rewrite-imports.ts
*/
export {default as dynamic, noSSR} from "next/dynamic"

View File

@@ -1 +0,0 @@
export {default as ErrorComponent} from "next/error"

View File

@@ -1,6 +0,0 @@
/*
* IF YOU CHANGE THIS FILE
* You also need to update the rewrite map in
* packages/babel-preset/src/rewrite-imports.ts
*/
export {default as Head} from "next/head"

View File

@@ -1,8 +0,0 @@
export * from "./types"
export * from "./router"
export * from "./error"
export * from "./error-boundary"
export {withBlitzAppRoot} from "./blitz-app-root"
export {validateZodSchema, formatZodError} from "./utils/index"
export {enhancePrisma} from "./prisma-utils"
export {Routes} from ".blitz"

View File

@@ -1,53 +0,0 @@
import {spawn} from "cross-spawn"
import which from "npm-which"
interface Constructor<T = unknown> {
new (...args: never[]): T
}
interface EnhancedPrismaClientAddedMethods {
$reset: () => Promise<void>
}
interface EnhancedPrismaClientConstructor<TPrismaClientCtor extends Constructor> {
new (...args: ConstructorParameters<TPrismaClientCtor>): InstanceType<TPrismaClientCtor> &
EnhancedPrismaClientAddedMethods
}
export const enhancePrisma = <TPrismaClientCtor extends Constructor>(
client: TPrismaClientCtor,
): EnhancedPrismaClientConstructor<TPrismaClientCtor> => {
return new Proxy(client as EnhancedPrismaClientConstructor<TPrismaClientCtor>, {
construct(target, args) {
if (typeof window !== "undefined" && process.env.JEST_WORKER_ID === undefined) {
// Return object with $use method if in the browser
// Skip in Jest tests because window is defined in Jest tests
return {$use: () => {}}
}
if (!global._blitz_prismaClient) {
const client = new target(...(args as any))
client.$reset = async function reset() {
if (process.env.NODE_ENV === "production") {
throw new Error(
"You are calling db.$reset() in a production environment. We think you probably didn't mean to do that, so we are throwing this error instead of destroying your life's work.",
)
}
const prismaBin = which(process.cwd()).sync("prisma")
await new Promise((res, rej) => {
const process = spawn(prismaBin, ["migrate", "reset", "--force", "--skip-generate"], {
stdio: "ignore",
})
process.on("exit", (code) => (code === 0 ? res(0) : rej(code)))
})
global._blitz_prismaClient.$disconnect()
}
global._blitz_prismaClient = client
}
return global._blitz_prismaClient
},
})
}

View File

@@ -1,69 +0,0 @@
import {
default as NextRouter,
NextRouter as NextRouterType,
useRouter as useNextRouter,
withRouter as withNextRouter,
} from "next/router"
import React from "react"
import {extractRouterParams, useParams, useRouterQuery} from "./router-hooks"
export const Router = NextRouter
export {createRouter, makePublicRouterInstance} from "next/router"
export {RouterContext} from "next/dist/shared/lib/router-context"
export {useParam, useParams, useRouterQuery} from "./router-hooks"
export interface BlitzRouter extends NextRouterType {
params: ReturnType<typeof extractRouterParams>
query: ReturnType<typeof useRouterQuery>
}
export interface WithRouterProps {
router: BlitzRouter
}
/**
* `withRouter` is a higher-order component that takes a component and returns a new one
* with an additional `router` prop.
*
* @example
* ```
* import {withRouter} from "blitz"
*
* function Page({router}) {
* return <p>{router.pathname}</p>
* }
*
* export default withRouter(Page)
* ```
*
* @param WrappedComponent - a React component that needs `router` object in props
* @returns A component with a `router` object in props
* @see Docs {@link https://blitzjs.com/docs/router#router-object | router}
*/
export const withRouter: typeof withNextRouter = (WrappedComponent) => {
function Wrapper({router, ...props}: any) {
const query = useRouterQuery()
const params = useParams()
return <WrappedComponent router={{...router, query, params}} {...props} />
}
return withNextRouter(Wrapper)
}
/**
* `useRouter` is a React hook used to access `router` object within components
*
* @returns `router` object
* @see Docs {@link https://blitzjs.com/docs/router#router-object | router}
*/
export function useRouter() {
const router = useNextRouter()
const query = useRouterQuery()
const params = useParams()
// TODO - we have to explicitly define the return type otherwise TS complains about
// NextHistoryState and TransitionOptions not being exported from Next.js code
return React.useMemo(() => {
return {...router, query, params}
}, [params, query, router]) as BlitzRouter
}

View File

@@ -1,143 +0,0 @@
import fromPairs from "lodash.frompairs"
import {useRouter} from "next/router"
import {useMemo} from "react"
import {Dict, ParsedUrlQuery, ParsedUrlQueryValue} from "../types"
export function useRouterQuery() {
const router = useRouter()
const query = useMemo(() => {
const query = decode(router.asPath.split("?", 2)[1])
return query
}, [router.asPath])
return query
}
function areQueryValuesEqual(value1: ParsedUrlQueryValue, value2: ParsedUrlQueryValue) {
// Check if their type match
if (typeof value1 !== typeof value2) {
return false
}
if (Array.isArray(value1) && Array.isArray(value2)) {
if (value1.length !== value2.length) {
return false
}
for (let i = 0; i < value1.length; i++) {
if (value1[i] !== value2[i]) {
return false
}
}
return true
}
return value1 === value2
}
export function extractRouterParams(routerQuery: ParsedUrlQuery, query: ParsedUrlQuery) {
return fromPairs(
Object.entries(routerQuery).filter(
([key, value]) =>
typeof query[key] === "undefined" || !areQueryValuesEqual(value, query[key]),
),
)
}
type ReturnTypes = "string" | "number" | "array"
export function useParams(): Dict<string | string[]>
export function useParams(returnType?: ReturnTypes): Dict<string | string[]>
export function useParams(returnType: "string"): Dict<string>
export function useParams(returnType: "number"): Dict<number>
export function useParams(returnType: "array"): Dict<string[]>
export function useParams(returnType?: "string" | "number" | "array" | undefined) {
const router = useRouter()
const query = useRouterQuery()
const params = useMemo(() => {
const rawParams = extractRouterParams(router.query, query)
if (returnType === "string") {
const params: Dict<string> = {}
for (const key in rawParams) {
if (typeof rawParams[key] === "string") {
params[key] = rawParams[key] as string
}
}
return params
}
if (returnType === "number") {
const params: Dict<number> = {}
for (const key in rawParams) {
if (rawParams[key]) {
const num = Number(rawParams[key])
params[key] = isNaN(num) ? undefined : num
}
}
return params
}
if (returnType === "array") {
const params: Dict<string[]> = {}
for (const key in rawParams) {
const rawValue = rawParams[key]
if (Array.isArray(rawParams[key])) {
params[key] = rawValue as string[]
} else if (typeof rawValue === "string") {
params[key] = [rawValue]
}
}
return params
}
return rawParams
}, [router.query, query, returnType])
return params
}
export function useParam(key: string): undefined | string | string[]
export function useParam(key: string, returnType: "string"): string | undefined
export function useParam(key: string, returnType: "number"): number | undefined
export function useParam(key: string, returnType: "array"): string[] | undefined
export function useParam(
key: string,
returnType?: ReturnTypes,
): undefined | number | string | string[] {
const params = useParams(returnType)
const value = params[key]
return value
}
/*
* Based on the code of https://github.com/lukeed/qss
*/
const decodeString = (str: string) => decodeURIComponent(str.replace(/\+/g, "%20"))
function decode(str: string) {
if (!str) return {}
let out: Record<string, string | string[]> = {}
for (const current of str.split("&")) {
let [key, value = ""] = current.split("=")
key = decodeString(key)
value = decodeString(value)
if (key.length === 0) continue
if (key in out) {
out[key] = ([] as string[]).concat(out[key], value)
} else {
out[key] = value
}
}
return out
}

View File

@@ -1,7 +0,0 @@
/*
* IF YOU CHANGE THIS FILE
* You also need to update the rewrite map in
* packages/babel-preset/src/rewrite-imports.ts
*/
export {resolver} from "./resolver"
export type {AuthenticatedMiddlewareCtx} from "./resolver"

View File

@@ -1,62 +0,0 @@
import {Ctx} from "next/types"
import {z} from "zod"
import {ParserType, resolver} from "./resolver"
describe("resolver", () => {
it("should typecheck and pass along value", async () => {
await resolverTest({})
})
it("should typecheck and pass along value if sync resolver is specified", async () => {
await resolverTest({type: "sync"})
})
it("should typecheck and pass along value if async resolver is specified", async () => {
await resolverTest({type: "async"})
})
})
const syncResolver = resolver.pipe(
resolver.zod(
z.object({
email: z.string().email(),
}),
"sync",
),
resolver.authorize({}),
(input) => {
return input.email
},
)
const asyncResolver = resolver.pipe(
resolver.zod(
z.object({
email: z.string().email(),
}),
"async",
),
resolver.authorize({}),
(input) => {
return input.email
},
)
const resolverTest = async ({type}: {type?: ParserType}) => {
const resolver1 = type === "sync" ? syncResolver : asyncResolver
const result1 = await resolver1(
{email: "test@example.com"},
{session: {$authorize: () => undefined} as Ctx},
)
expect(result1).toBe("test@example.com")
const resolver2 = resolver.pipe(
/*resolver.authorize(), */ (input: {email: string}) => {
return input.email
},
)
const result2 = await resolver2(
{email: "test@example.com"},
{session: {$authorize: () => undefined} as Ctx},
)
expect(result2).toBe("test@example.com")
}

View File

@@ -1,26 +0,0 @@
export const suspend = <T>(promise: Promise<T>) => {
let result: any
let status = "pending"
const suspender = promise.then(
(response) => {
status = "success"
result = response
},
(error) => {
status = "error"
result = error
},
)
return (): T => {
switch (status) {
case "pending":
throw suspender
case "error":
throw result
default:
return result
}
}
}

View File

@@ -1,29 +0,0 @@
export type {BlitzConfig} from "@blitzjs/config"
export type QueryFn = (...args: any) => Promise<any>
export type Dict<T> = Record<string, T | undefined>
export type ParsedUrlQuery = Dict<string | string[]>
export type ParsedUrlQueryValue = string | string[] | undefined
export type Options = {
fromQueryHook?: boolean
}
// The actual resolver source definition
export type Resolver<TInput, TResult> = (input: TInput, ctx?: any) => Promise<TResult>
declare global {
namespace NodeJS {
interface Global {
_blitz_prismaClient: any
}
}
}
export interface ErrorFallbackProps {
error: Error & Record<any, any>
resetErrorBoundary: (...args: Array<unknown>) => void
}

View File

@@ -1,7 +0,0 @@
import {useEffect, useLayoutEffect} from "react"
const isServer = typeof window === "undefined"
// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser.
export const useIsomorphicLayoutEffect = isServer ? useEffect : useLayoutEffect

View File

@@ -1,98 +0,0 @@
import {z} from "zod"
import {formatZodError, validateZodSchema} from "./index"
const validateSchema = (schema: any, input: any) => {
const result = schema.safeParse(input)
if (result.success) throw new Error("Schema should not return success")
return result
}
const Schema = z.object({
test: z.string(),
})
describe("formatZodError", () => {
it("formats the zod error", () => {
expect(formatZodError(validateSchema(Schema, {}).error)).toEqual({
test: "Required",
})
})
it("formats the nested zod error", () => {
const NestedSchema = z.object({
test: z.string(),
nested: z.object({
foo: z.string(),
test: z.string(),
}),
})
const result = validateSchema(NestedSchema, {test: "yo", nested: {foo: "yo"}})
expect(formatZodError(result.error)).toEqual({
nested: {test: "Required"},
})
})
it("formats 2 levels nested zod error", () => {
const DoubleNestedSchema = z.object({
test: z.string(),
nested: z.object({
test: z.string(),
doubleNested: z.object({
test: z.string(),
}),
}),
})
expect(
formatZodError(
validateSchema(DoubleNestedSchema, {
nested: {doubleNested: {}},
}).error,
),
).toEqual({
test: "Required",
nested: {test: "Required", doubleNested: {test: "Required"}},
})
})
it("formats arrays", () => {
const NestedSchema = z.object({
students: z.array(
z.object({
name: z.string(),
}),
),
data: z.object({
1: z.literal(true),
}),
})
const result = validateSchema(NestedSchema, {
students: [{name: "hi"}, {wat: true}, {name: true}],
data: {},
})
expect(formatZodError(result.error)).toEqual({
students: [undefined, {name: "Required"}, {name: "Expected string, received boolean"}],
data: [undefined, "Expected true, received undefined"],
})
})
})
describe("validateZodSchema", () => {
it("passes validation", async () => {
expect(await validateZodSchema(Schema)({test: "test"})).toEqual({})
})
it("fails validation", async () => {
expect(await validateZodSchema(Schema)({})).toEqual({test: "Required"})
})
it("passes validation if synchronous", () => {
expect(validateZodSchema(Schema, "sync")({test: "test"})).toEqual({})
})
it("fails validation if synchronous", () => {
expect(validateZodSchema(Schema, "sync")({})).toEqual({test: "Required"})
})
})

View File

@@ -1,20 +0,0 @@
import {prettyMs} from "./pretty-ms"
describe("prettyMs", () => {
it("returns pretty strings", () => {
// ms
expect(prettyMs(0)).toMatchInlineSnapshot(`"0ms"`)
expect(prettyMs(200)).toMatchInlineSnapshot(`"200ms"`)
// seconds
expect(prettyMs(1000)).toMatchInlineSnapshot(`"1s"`)
expect(prettyMs(1000)).toMatchInlineSnapshot(`"1s"`)
expect(prettyMs(1600)).toMatchInlineSnapshot(`"1.6s"`)
expect(prettyMs(1500)).toMatchInlineSnapshot(`"1.5s"`)
expect(prettyMs(1666)).toMatchInlineSnapshot(`"1.7s"`)
// negative
expect(prettyMs(-1)).toMatchInlineSnapshot(`"-1ms"`)
expect(prettyMs(-2000)).toMatchInlineSnapshot(`"-2s"`)
})
})

View File

@@ -1,20 +0,0 @@
function round(num: number, decimalPlaces: number) {
const p = Math.pow(10, decimalPlaces)
const m = num * p * (1 + Number.EPSILON)
return Math.round(m) / p
}
/**
* Formats milliseconds to a string
* If more than 1s, it'll return seconds instead
* @example
* prettyMs(100) // -> `100ms`
* prettyMs(1200) // -> `1.2s`
* @param ms
*/
export function prettyMs(ms: number): string {
if (Math.abs(ms) >= 1000) {
return `${round(ms / 1000, 1)}s`
}
return `${ms}ms`
}

View File

@@ -1,215 +0,0 @@
import {extractRouterParams, useParam, useParams, useRouterQuery} from "../src/router/router-hooks"
import {renderHook} from "./test-utils"
describe("useRouterQuery", () => {
it("returns proper values", () => {
const {result} = renderHook(() => useRouterQuery(), {
router: {asPath: "/?foo=foo&num=0&bool=true&float=1.23&empty"},
})
expect(result.current).toEqual({
foo: "foo",
num: "0",
bool: "true",
float: "1.23",
empty: "",
})
})
it("decode correctly", () => {
const {result} = renderHook(() => useRouterQuery(), {
router: {asPath: "/?encoded=D%C3%A9j%C3%A0%20vu&spaces=Hello+World&both=Hola%2C+Mundo%21"},
})
expect(result.current).toEqual({
encoded: "Déjà vu",
spaces: "Hello World",
both: "Hola, Mundo!",
})
})
})
describe("extractRouterParams", () => {
it("returns proper params", () => {
const routerQuery = {
id: "1",
cat: "category",
slug: ["example", "multiple", "slugs"],
empty: "",
queryArray: ["1", "123", ""],
}
const query = {
cat: "somethingelse",
slug: ["query-slug"],
queryArray: ["1", "123", ""],
onlyInQuery: "onlyInQuery",
}
const params = extractRouterParams(routerQuery, query)
expect(params).toEqual({
id: "1",
cat: "category",
slug: ["example", "multiple", "slugs"],
empty: "",
})
})
})
describe("useParams", () => {
it("works without parameter", () => {
// This is the router query object which includes route params
const query = {
id: "1",
cat: "category",
slug: ["example", "multiple", "slugs"],
empty: "",
}
const {result} = renderHook(() => useParams(), {router: {query}})
expect(result.current).toEqual({
id: "1",
cat: "category",
slug: ["example", "multiple", "slugs"],
empty: "",
})
})
it("works with string", () => {
// This is the router query object which includes route params
const query = {
id: "1",
cat: "category",
slug: ["example", "multiple", "slugs"],
empty: "",
}
const {result} = renderHook(() => useParams("string"), {router: {query}})
expect(result.current).toEqual({
id: "1",
cat: "category",
empty: "",
})
})
it("works with number", () => {
// This is the router query object which includes route params
const query = {
id: "1",
cat: "category",
slug: ["example", "multiple", "slugs"],
empty: "",
}
const {result} = renderHook(() => useParams("number"), {router: {query}})
expect(result.current).toEqual({
id: 1,
cat: undefined,
slug: undefined,
})
})
it("works with array", () => {
// This is the router query object which includes route params
const query = {
id: "1",
cat: "category",
slug: ["example", "multiple", "slugs"],
empty: "",
}
const {result} = renderHook(() => useParams("array"), {router: {query}})
expect(result.current).toEqual({
id: ["1"],
cat: ["category"],
slug: ["example", "multiple", "slugs"],
empty: [""],
})
})
})
describe("useParam", () => {
it("works without parameter", () => {
// This is the router query object which includes route params
const query = {
id: "1",
cat: "category",
slug: ["example", "multiple", "slugs"],
empty: "",
}
let {result} = renderHook(() => useParam("id"), {router: {query}})
expect(result.current).toEqual("1")
;({result} = renderHook(() => useParam("cat"), {router: {query}}))
expect(result.current).toEqual("category")
;({result} = renderHook(() => useParam("slug"), {router: {query}}))
expect(result.current).toEqual(["example", "multiple", "slugs"])
;({result} = renderHook(() => useParam("empty"), {router: {query}}))
expect(result.current).toEqual("")
;({result} = renderHook(() => useParam("doesnt-exist"), {router: {query}}))
expect(result.current).toBeUndefined()
})
it("works with string", () => {
// This is the router query object which includes route params
const query = {
id: "1",
cat: "category",
slug: ["example", "multiple", "slugs"],
empty: "",
}
let {result} = renderHook(() => useParam("id", "string"), {router: {query}})
expect(result.current).toEqual("1")
;({result} = renderHook(() => useParam("cat", "string"), {router: {query}}))
expect(result.current).toEqual("category")
;({result} = renderHook(() => useParam("slug", "string"), {router: {query}}))
expect(result.current).toEqual(undefined)
;({result} = renderHook(() => useParam("empty", "string"), {router: {query}}))
expect(result.current).toEqual("")
;({result} = renderHook(() => useParam("doesnt-exist", "string"), {router: {query}}))
expect(result.current).toBeUndefined()
})
it("works with number", () => {
// This is the router query object which includes route params
const query = {
id: "1",
cat: "category",
slug: ["example", "multiple", "slugs"],
empty: "",
}
let {result} = renderHook(() => useParam("id", "number"), {router: {query}})
expect(result.current).toEqual(1)
;({result} = renderHook(() => useParam("cat", "number"), {router: {query}}))
expect(result.current).toBeUndefined()
;({result} = renderHook(() => useParam("slug", "number"), {router: {query}}))
expect(result.current).toBeUndefined()
;({result} = renderHook(() => useParam("empty", "number"), {router: {query}}))
expect(result.current).toBeUndefined()
;({result} = renderHook(() => useParam("doesnt-exist", "number"), {router: {query}}))
expect(result.current).toBeUndefined()
})
it("works with array", () => {
// This is the router query object which includes route params
const query = {
id: "1",
cat: "category",
slug: ["example", "multiple", "slugs"],
empty: "",
}
let {result} = renderHook(() => useParam("id", "array"), {router: {query}})
expect(result.current).toEqual(["1"])
;({result} = renderHook(() => useParam("cat", "array"), {router: {query}}))
expect(result.current).toEqual(["category"])
;({result} = renderHook(() => useParam("slug", "array"), {router: {query}}))
expect(result.current).toEqual(["example", "multiple", "slugs"])
;({result} = renderHook(() => useParam("empty", "array"), {router: {query}}))
expect(result.current).toEqual([""])
;({result} = renderHook(() => useParam("doesnt-exist", "array"), {router: {query}}))
expect(result.current).toBeUndefined()
})
})

View File

@@ -1,124 +0,0 @@
import {render as defaultRender} from "@testing-library/react"
import {renderHook as defaultRenderHook} from "@testing-library/react-hooks"
import {BlitzProvider, queryClient} from "next/data-client"
import {RouterContext} from "next/dist/shared/lib/router-context"
import {NextRouter} from "next/router"
import React from "react"
export * from "@testing-library/react"
// --------------------------------------------------
// Override the default test render with our own
//
// You can override the router mock like this:
//
// const { baseElement } = render(<MyComponent />, {
// router: { pathname: '/my-custom-pathname' },
// });
// --------------------------------------------------
type DefaultParams = Parameters<typeof defaultRender>
type RenderUI = DefaultParams[0]
type RenderOptions = DefaultParams[1] & {
router?: Partial<NextRouter>
dehydratedState?: unknown
}
const mockRouter: NextRouter = {
basePath: "",
pathname: "/",
route: "/",
asPath: "/",
query: {},
isReady: true,
isLocaleDomain: false,
isPreview: false,
push: jest.fn(),
replace: jest.fn(),
reload: jest.fn(),
back: jest.fn(),
prefetch: jest.fn(),
beforePopState: jest.fn(),
events: {
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
},
isFallback: false,
}
export function render(
ui: RenderUI,
{wrapper, router, dehydratedState, ...options}: RenderOptions = {},
) {
if (!wrapper) {
wrapper = ({children}) => (
<BlitzProvider client={queryClient} dehydratedState={dehydratedState}>
<RouterContext.Provider value={{...mockRouter, ...router}}>
{children}
</RouterContext.Provider>
</BlitzProvider>
)
}
return defaultRender(ui, {wrapper, ...options})
}
// --------------------------------------------------
// Override the default test renderHook with our own
//
// You can override the router mock like this:
//
// const result = renderHook(() => myHook(), {
// router: { pathname: '/my-custom-pathname' },
// });
// --------------------------------------------------
type DefaultHookParams = Parameters<typeof defaultRenderHook>
type RenderHook = DefaultHookParams[0]
type RenderHookOptions = DefaultHookParams[1] & {
router?: Partial<NextRouter>
dehydratedState?: unknown
}
export function renderHook(
hook: RenderHook,
{wrapper, router, dehydratedState, ...options}: RenderHookOptions = {},
) {
if (!wrapper) {
wrapper = ({children}) => (
<BlitzProvider client={queryClient} dehydratedState={dehydratedState}>
<RouterContext.Provider value={{...mockRouter, ...router}}>
{children}
</RouterContext.Provider>
</BlitzProvider>
)
}
return defaultRenderHook(hook, {wrapper, ...options})
}
// This enhance fn does what getIsomorphicEnhancedResolver does during build time
export function enhanceQueryFn(fn: any) {
const newFn = (...args: any) => {
const [data, ...rest] = args
return fn(data, ...rest)
}
newFn._meta = {
name: "testResolver",
type: "query",
path: "app/test",
apiUrl: "test/url",
}
return newFn
}
// This enhance fn does what getIsomorphicEnhancedResolver does during build time
export function enhanceMutationFn(fn: any) {
const newFn = (...args: any) => fn(...args)
newFn._meta = {
name: "testResolver",
type: "mutation",
path: "app/test",
apiUrl: "test/url",
}
return newFn
}

View File

@@ -1,7 +0,0 @@
// declare module '@prisma/client' {
// export class PrismaClient {
// constructor(args: any)
// }
// }
export * from "../src/types"

View File

@@ -20,7 +20,6 @@
],
"dependencies": {
"@blitzjs/config": "0.41.2-canary.1",
"@blitzjs/core": "0.41.2-canary.1",
"@blitzjs/display": "0.41.2-canary.1",
"cross-spawn": "7.0.3",
"detect-port": "1.3.0",

View File

@@ -10,7 +10,7 @@ nextJson.blitzVersion = nextJson.version
nextJson.version = `${nextJson.nextjsVersion}-${nextJson.blitzVersion}`
fs.writeJSONSync(nextJsonPath, nextJson, {spaces: 2})
const blitzCoreJsonPath = "packages/core/package.json"
const blitzCoreJsonPath = "packages/blitz/package.json"
const blitzCoreJson = fs.readJSONSync(blitzCoreJsonPath)
blitzCoreJson.dependencies.next = `npm:@blitzjs/next@${nextJson.version}`
fs.writeJSONSync(blitzCoreJsonPath, blitzCoreJson, {spaces: 2})

View File

@@ -4636,16 +4636,18 @@
dependencies:
"@types/lodash" "*"
"@types/lodash.frompairs@4.0.6":
version "4.0.6"
resolved "https://registry.yarnpkg.com/@types/lodash.frompairs/-/lodash.frompairs-4.0.6.tgz#09b082c10fa753dc2001302b75ac79ca1e0a9ea3"
integrity sha512-rwCUf4NMKhXpiVjL/RXP8YOk+rd02/J4tACADEgaMXRVnzDbSSlBMKFZoX/ARmHVLg3Qc98Um4PErGv8FbxU7w==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
version "4.14.166"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.166.tgz#07e7f2699a149219dbc3c35574f126ec8737688f"
integrity sha512-A3YT/c1oTlyvvW/GQqG86EyqWNrT/tisOIh2mW3YCgcx71TNjiTZA3zYZWA5BCmtsOTXjhliy4c4yEkErw6njA==
"@types/lodash@4.14.149":
version "4.14.149"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440"
integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==
"@types/long@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
@@ -12542,11 +12544,6 @@ html-to-text@6.0.0:
lodash "^4.17.20"
minimist "^1.2.5"
htmlescape@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
integrity sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=
htmlparser2@5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-5.0.1.tgz#7daa6fc3e35d6107ac95a4fc08781f091664f6e7"
@@ -24633,10 +24630,10 @@ zip-stream@^3.0.1:
compress-commons "^3.0.0"
readable-stream "^3.6.0"
zod@3.8.1:
version "3.8.1"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.8.1.tgz#b1173c3b4ac2a9e06d302ff580e3b41902766b9f"
integrity sha512-u4Uodl7dLh8nXZwqXL1SM5FAl5b4lXYHOxMUVb9lqhlEAZhA2znX+0oW480m0emGFMxpoRHzUncAqRkc4h8ZJA==
zod@3.10.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.10.1.tgz#ea5fdbb9d6ed0abc3c3000be9b768d692c2d5275"
integrity sha512-ZnIzDr3vhppKW7yTAlvUQ7QJir5yoL14DgZ6DjYNb4/D4DxFdZeysF6Q5hAahU7KXtaiTgYkGO/qiAD83YN3ig==
zwitch@^1.0.0:
version "1.0.5"