Move internal @blitzjs/core package into nextjs fork core (meta) (#2857)
This commit is contained in:
@@ -1 +1 @@
|
|||||||
12.20.0
|
14.18.1
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {render} from "test/utils"
|
import {render} from "test/utils"
|
||||||
import Home from "./index"
|
import Home from "./index"
|
||||||
|
|
||||||
jest.mock("@blitzjs/core", () => ({
|
jest.mock("next/data-client", () => ({
|
||||||
...jest.requireActual<object>("@blitzjs/core")!,
|
...jest.requireActual<object>("next/data-client")!,
|
||||||
useQuery: () => [
|
useQuery: () => [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { render } from "test/utils"
|
import { render } from "test/utils"
|
||||||
import Home from "./index"
|
import Home from "./index"
|
||||||
|
|
||||||
jest.mock("@blitzjs/core", () => ({
|
jest.mock("next/data-client", () => ({
|
||||||
...jest.requireActual<object>("@blitzjs/core")!,
|
...jest.requireActual<object>("next/data-client")!,
|
||||||
useQuery: () => [
|
useQuery: () => [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
"react": "0.0.0-experimental-6a589ad71",
|
"react": "0.0.0-experimental-6a589ad71",
|
||||||
"react-dom": "0.0.0-experimental-6a589ad71",
|
"react-dom": "0.0.0-experimental-6a589ad71",
|
||||||
"react-final-form": "6.5.2",
|
"react-final-form": "6.5.2",
|
||||||
"zod": "3.8.1"
|
"zod": "3.10.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/cypress": "8.0.1",
|
"@testing-library/cypress": "8.0.1",
|
||||||
|
|||||||
1
nextjs/.node-version
Normal file
1
nextjs/.node-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
14.18.1
|
||||||
@@ -152,10 +152,10 @@ export async function saveRouteManifest(
|
|||||||
async function findNodeModulesRoot(src: string) {
|
async function findNodeModulesRoot(src: string) {
|
||||||
/*
|
/*
|
||||||
* Because of our package structure, and because of how things like pnpm link modules,
|
* 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
|
* we must first find blitz package, and then find `next` and then
|
||||||
* the root of @blitzjs/core
|
* 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
|
* If that changes, then this logic here will need to change
|
||||||
*/
|
*/
|
||||||
manifestDebug('src ' + src)
|
manifestDebug('src ' + src)
|
||||||
@@ -189,13 +189,13 @@ async function findNodeModulesRoot(src: string) {
|
|||||||
}
|
}
|
||||||
const blitzCorePkgLocation = dirname(
|
const blitzCorePkgLocation = dirname(
|
||||||
(await findUp('package.json', {
|
(await findUp('package.json', {
|
||||||
cwd: resolveFrom(blitzPkgLocation, '@blitzjs/core'),
|
cwd: resolveFrom(blitzPkgLocation, 'next'),
|
||||||
})) ?? ''
|
})) ?? ''
|
||||||
)
|
)
|
||||||
manifestDebug('blitzCorePkgLocation ' + blitzCorePkgLocation)
|
manifestDebug('blitzCorePkgLocation ' + blitzCorePkgLocation)
|
||||||
if (!blitzCorePkgLocation) {
|
if (!blitzCorePkgLocation) {
|
||||||
throw new Error(
|
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, '../../')
|
root = join(blitzCorePkgLocation, '../../')
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
/* global window */
|
/* global window */
|
||||||
import React from 'react'
|
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 type { NextRouter } from '../shared/lib/router/router'
|
||||||
import { RouterContext } from '../shared/lib/router-context'
|
import { RouterContext } from '../shared/lib/router-context'
|
||||||
|
|
||||||
@@ -17,6 +20,7 @@ type SingletonRouterBase = {
|
|||||||
export { Router }
|
export { Router }
|
||||||
|
|
||||||
export type { NextRouter }
|
export type { NextRouter }
|
||||||
|
export type BlitzRouter = NextRouter
|
||||||
|
|
||||||
export type SingletonRouter = SingletonRouterBase & NextRouter
|
export type SingletonRouter = SingletonRouterBase & NextRouter
|
||||||
|
|
||||||
@@ -177,3 +181,86 @@ export function makePublicRouterInstance(router: Router): NextRouter {
|
|||||||
|
|
||||||
return instance
|
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
|
||||||
|
}
|
||||||
|
|||||||
22
nextjs/packages/next/compiled/lodash.frompairs/LICENSE
Normal file
22
nextjs/packages/next/compiled/lodash.frompairs/LICENSE
Normal 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.
|
||||||
1
nextjs/packages/next/compiled/lodash.frompairs/index.js
Normal file
1
nextjs/packages/next/compiled/lodash.frompairs/index.js
Normal 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)})();
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"name":"lodash.frompairs","main":"index.js","author":"John-David Dalton <john.david.dalton@gmail.com> (http://allyoucanleet.com/)","license":"MIT"}
|
||||||
6
nextjs/packages/next/lib-types/lodash.frompairs.d.ts
vendored
Normal file
6
nextjs/packages/next/lib-types/lodash.frompairs.d.ts
vendored
Normal 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>
|
||||||
|
}
|
||||||
3
nextjs/packages/next/lib-types/micromatch.d.ts
vendored
Normal file
3
nextjs/packages/next/lib-types/micromatch.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
declare module 'micromatch' {
|
||||||
|
export function isMatch(source: string, patterns: string[]): boolean
|
||||||
|
}
|
||||||
176
nextjs/packages/next/lib-types/npm-which.d.ts
vendored
Normal file
176
nextjs/packages/next/lib-types/npm-which.d.ts
vendored
Normal 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
|
||||||
|
}
|
||||||
@@ -92,6 +92,7 @@
|
|||||||
"chokidar": "3.5.1",
|
"chokidar": "3.5.1",
|
||||||
"constants-browserify": "1.0.0",
|
"constants-browserify": "1.0.0",
|
||||||
"cookie-session": "^1.4.0",
|
"cookie-session": "^1.4.0",
|
||||||
|
"cross-spawn": "7.0.3",
|
||||||
"crypto-browserify": "3.12.0",
|
"crypto-browserify": "3.12.0",
|
||||||
"cssnano-simple": "3.0.0",
|
"cssnano-simple": "3.0.0",
|
||||||
"debug": "4.3.1",
|
"debug": "4.3.1",
|
||||||
@@ -108,6 +109,7 @@
|
|||||||
"node-fetch": "2.6.1",
|
"node-fetch": "2.6.1",
|
||||||
"node-html-parser": "1.4.9",
|
"node-html-parser": "1.4.9",
|
||||||
"node-libs-browser": "^2.2.1",
|
"node-libs-browser": "^2.2.1",
|
||||||
|
"npm-which": "^3.0.1",
|
||||||
"null-loader": "4.0.1",
|
"null-loader": "4.0.1",
|
||||||
"os-browserify": "0.3.0",
|
"os-browserify": "0.3.0",
|
||||||
"p-limit": "3.1.0",
|
"p-limit": "3.1.0",
|
||||||
@@ -192,6 +194,7 @@
|
|||||||
"@types/fresh": "0.5.0",
|
"@types/fresh": "0.5.0",
|
||||||
"@types/jsonwebtoken": "8.5.0",
|
"@types/jsonwebtoken": "8.5.0",
|
||||||
"@types/lodash.curry": "4.1.6",
|
"@types/lodash.curry": "4.1.6",
|
||||||
|
"@types/lodash.frompairs": "4.0.6",
|
||||||
"@types/lru-cache": "5.1.0",
|
"@types/lru-cache": "5.1.0",
|
||||||
"@types/node-fetch": "2.5.8",
|
"@types/node-fetch": "2.5.8",
|
||||||
"@types/path-to-regexp": "1.7.0",
|
"@types/path-to-regexp": "1.7.0",
|
||||||
@@ -221,7 +224,6 @@
|
|||||||
"conf": "5.0.0",
|
"conf": "5.0.0",
|
||||||
"content-type": "1.0.4",
|
"content-type": "1.0.4",
|
||||||
"cookie": "0.4.1",
|
"cookie": "0.4.1",
|
||||||
"cross-spawn": "7.0.3",
|
|
||||||
"css-loader": "4.3.0",
|
"css-loader": "4.3.0",
|
||||||
"devalue": "2.0.1",
|
"devalue": "2.0.1",
|
||||||
"escape-string-regexp": "2.0.0",
|
"escape-string-regexp": "2.0.0",
|
||||||
@@ -238,6 +240,7 @@
|
|||||||
"jsonwebtoken": "8.5.1",
|
"jsonwebtoken": "8.5.1",
|
||||||
"loader-utils": "2.0.0",
|
"loader-utils": "2.0.0",
|
||||||
"lodash.curry": "4.1.1",
|
"lodash.curry": "4.1.1",
|
||||||
|
"lodash.frompairs": "4.0.1",
|
||||||
"lru-cache": "5.1.1",
|
"lru-cache": "5.1.1",
|
||||||
"mini-css-extract-plugin": "1.5.0",
|
"mini-css-extract-plugin": "1.5.0",
|
||||||
"nanoid": "^3.1.20",
|
"nanoid": "^3.1.20",
|
||||||
@@ -264,7 +267,8 @@
|
|||||||
"unistore": "3.4.1",
|
"unistore": "3.4.1",
|
||||||
"web-vitals": "2.1.0",
|
"web-vitals": "2.1.0",
|
||||||
"webpack": "4.44.1",
|
"webpack": "4.44.1",
|
||||||
"webpack-sources": "1.4.3"
|
"webpack-sources": "1.4.3",
|
||||||
|
"zod": "3.10.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ async function appGetInitialProps({
|
|||||||
return { pageProps }
|
return { pageProps }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class App<P = {}, CP = {}, S = {}> extends React.Component<
|
export class App<P = {}, CP = {}, S = {}> extends React.Component<
|
||||||
P & AppProps<CP>,
|
P & AppProps<CP>,
|
||||||
S
|
S
|
||||||
> {
|
> {
|
||||||
@@ -41,3 +41,4 @@ export default class App<P = {}, CP = {}, S = {}> extends React.Component<
|
|||||||
return <Component {...pageProps} />
|
return <Component {...pageProps} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export default App
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function _getInitialProps({
|
|||||||
/**
|
/**
|
||||||
* `Error` component used for handling errors.
|
* `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 displayName = 'ErrorPage'
|
||||||
|
|
||||||
static getInitialProps = _getInitialProps
|
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 } = {
|
const styles: { [k: string]: React.CSSProperties } = {
|
||||||
error: {
|
error: {
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ import Loadable from '../shared/lib/loadable'
|
|||||||
import { LoadableContext } from '../shared/lib/loadable-context'
|
import { LoadableContext } from '../shared/lib/loadable-context'
|
||||||
import postProcess from '../shared/lib/post-process'
|
import postProcess from '../shared/lib/post-process'
|
||||||
import { RouterContext } from '../shared/lib/router-context'
|
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 { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic'
|
||||||
import {
|
import {
|
||||||
AppType,
|
AppType,
|
||||||
@@ -74,6 +78,7 @@ class ServerRouter implements NextRouter {
|
|||||||
route: string
|
route: string
|
||||||
pathname: string
|
pathname: string
|
||||||
query: ParsedUrlQuery
|
query: ParsedUrlQuery
|
||||||
|
params: ParsedUrlQuery
|
||||||
asPath: string
|
asPath: string
|
||||||
basePath: string
|
basePath: string
|
||||||
events: any
|
events: any
|
||||||
@@ -103,6 +108,7 @@ class ServerRouter implements NextRouter {
|
|||||||
this.route = pathname.replace(/\/$/, '') || '/'
|
this.route = pathname.replace(/\/$/, '') || '/'
|
||||||
this.pathname = pathname
|
this.pathname = pathname
|
||||||
this.query = query
|
this.query = query
|
||||||
|
this.params = extractRouterParams(query, extractQueryFromAsPath(as))
|
||||||
this.asPath = as
|
this.asPath = as
|
||||||
this.isFallback = isFallback
|
this.isFallback = isFallback
|
||||||
this.basePath = basePath
|
this.basePath = basePath
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function noSSR<P = {}>(
|
|||||||
|
|
||||||
// function dynamic<P = {}, O extends DynamicOptions>(options: O):
|
// function dynamic<P = {}, O extends DynamicOptions>(options: O):
|
||||||
|
|
||||||
export default function dynamic<P = {}>(
|
export function dynamic<P = {}>(
|
||||||
dynamicOptions: DynamicOptions<P> | Loader<P>,
|
dynamicOptions: DynamicOptions<P> | Loader<P>,
|
||||||
options?: DynamicOptions<P>
|
options?: DynamicOptions<P>
|
||||||
): React.ComponentType<P> {
|
): React.ComponentType<P> {
|
||||||
@@ -130,3 +130,4 @@ export default function dynamic<P = {}>(
|
|||||||
|
|
||||||
return loadableFn(loadableOptions)
|
return loadableFn(loadableOptions)
|
||||||
}
|
}
|
||||||
|
export default dynamic
|
||||||
|
|||||||
@@ -27,18 +27,21 @@ function onlyReactElement(
|
|||||||
// Adds support for React.Fragment
|
// Adds support for React.Fragment
|
||||||
if (child.type === React.Fragment) {
|
if (child.type === React.Fragment) {
|
||||||
return list.concat(
|
return list.concat(
|
||||||
React.Children.toArray(child.props.children).reduce((
|
React.Children.toArray(child.props.children).reduce(
|
||||||
fragmentList: Array<React.ReactElement<any>>,
|
(
|
||||||
fragmentChild: any // blitz :React.ReactChild
|
fragmentList: Array<React.ReactElement<any>>,
|
||||||
): Array<React.ReactElement<any>> => {
|
fragmentChild: any // blitz :React.ReactChild
|
||||||
if (
|
): Array<React.ReactElement<any>> => {
|
||||||
typeof fragmentChild === 'string' ||
|
if (
|
||||||
typeof fragmentChild === 'number'
|
typeof fragmentChild === 'string' ||
|
||||||
) {
|
typeof fragmentChild === 'number'
|
||||||
return fragmentList
|
) {
|
||||||
}
|
return fragmentList
|
||||||
return fragmentList.concat(fragmentChild)
|
}
|
||||||
}, []) as any //blitz
|
return fragmentList.concat(fragmentChild)
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
) as any //blitz
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return list.concat(child)
|
return list.concat(child)
|
||||||
@@ -144,10 +147,9 @@ function reduceComponents(
|
|||||||
c.type === 'link' &&
|
c.type === 'link' &&
|
||||||
c.props['href'] &&
|
c.props['href'] &&
|
||||||
// TODO(prateekbh@): Replace this with const from `constants` when the tree shaking works.
|
// TODO(prateekbh@): Replace this with const from `constants` when the tree shaking works.
|
||||||
[
|
['https://fonts.googleapis.com/css', 'https://use.typekit.net/'].some(
|
||||||
'https://fonts.googleapis.com/css',
|
(url) => c.props['href'].startsWith(url)
|
||||||
'https://use.typekit.net/',
|
)
|
||||||
].some((url) => c.props['href'].startsWith(url))
|
|
||||||
) {
|
) {
|
||||||
const newProps = { ...(c.props || {}) }
|
const newProps = { ...(c.props || {}) }
|
||||||
newProps['data-href'] = newProps['href']
|
newProps['data-href'] = newProps['href']
|
||||||
@@ -167,7 +169,7 @@ function reduceComponents(
|
|||||||
* This component injects elements to `<head>` of your page.
|
* 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.
|
* 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 ampState = useContext(AmpStateContext)
|
||||||
const headManager = useContext(HeadManagerContext)
|
const headManager = useContext(HeadManagerContext)
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { searchParamsToUrlQuery } from './utils/querystring'
|
|||||||
import resolveRewrites from './utils/resolve-rewrites'
|
import resolveRewrites from './utils/resolve-rewrites'
|
||||||
import { getRouteMatcher } from './utils/route-matcher'
|
import { getRouteMatcher } from './utils/route-matcher'
|
||||||
import { getRouteRegex } from './utils/route-regex'
|
import { getRouteRegex } from './utils/route-regex'
|
||||||
|
import fromPairs from 'next/dist/compiled/lodash.frompairs'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -405,6 +406,7 @@ export type BaseRouter = {
|
|||||||
route: string
|
route: string
|
||||||
pathname: string
|
pathname: string
|
||||||
query: ParsedUrlQuery
|
query: ParsedUrlQuery
|
||||||
|
params: ParsedUrlQuery
|
||||||
asPath: string
|
asPath: string
|
||||||
basePath: string
|
basePath: string
|
||||||
locale?: string
|
locale?: string
|
||||||
@@ -529,6 +531,7 @@ export default class Router implements BaseRouter {
|
|||||||
route: string
|
route: string
|
||||||
pathname: string
|
pathname: string
|
||||||
query: ParsedUrlQuery
|
query: ParsedUrlQuery
|
||||||
|
params: ParsedUrlQuery
|
||||||
asPath: string
|
asPath: string
|
||||||
basePath: string
|
basePath: string
|
||||||
|
|
||||||
@@ -630,6 +633,7 @@ export default class Router implements BaseRouter {
|
|||||||
this.pageLoader = pageLoader
|
this.pageLoader = pageLoader
|
||||||
this.pathname = pathname
|
this.pathname = pathname
|
||||||
this.query = query
|
this.query = query
|
||||||
|
this.params = extractRouterParams(query, extractQueryFromAsPath(as))
|
||||||
// if auto prerendered and dynamic route wait to update asPath
|
// if auto prerendered and dynamic route wait to update asPath
|
||||||
// until after mount to prevent hydration mismatch
|
// until after mount to prevent hydration mismatch
|
||||||
const autoExportDynamic =
|
const autoExportDynamic =
|
||||||
@@ -1442,6 +1446,7 @@ export default class Router implements BaseRouter {
|
|||||||
this.route = route
|
this.route = route
|
||||||
this.pathname = pathname
|
this.pathname = pathname
|
||||||
this.query = query
|
this.query = query
|
||||||
|
this.params = extractRouterParams(query, extractQueryFromAsPath(as))
|
||||||
this.asPath = as
|
this.asPath = as
|
||||||
return this.notify(data, resetScroll)
|
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])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
let runtimeConfig: any
|
let runtimeConfig: any
|
||||||
|
|
||||||
export default () => {
|
export const getConfig = () => {
|
||||||
return runtimeConfig
|
return runtimeConfig
|
||||||
}
|
}
|
||||||
|
export default getConfig
|
||||||
|
|
||||||
export function setConfig(configValue: any): void {
|
export function setConfig(configValue: any): void {
|
||||||
runtimeConfig = configValue
|
runtimeConfig = configValue
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export * from './middleware'
|
|||||||
export * from './auth-sessions'
|
export * from './auth-sessions'
|
||||||
export * from './auth-utils'
|
export * from './auth-utils'
|
||||||
export * from './passport-adapter'
|
export * from './passport-adapter'
|
||||||
|
export * from './resolver'
|
||||||
|
|
||||||
export function isLocalhost(req: NextApiRequest | IncomingMessage): boolean {
|
export function isLocalhost(req: NextApiRequest | IncomingMessage): boolean {
|
||||||
let { host } = req.headers
|
let { host } = req.headers
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import {AuthenticatedSessionContext, Ctx, SessionContext, SessionContextBase} from "next/types"
|
import {
|
||||||
import {Await, EnsurePromise} from "next/types/utils"
|
AuthenticatedSessionContext,
|
||||||
import type {input as zInput, output as zOutput, ZodTypeAny} from "zod"
|
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> {
|
interface ResultWithContext<Result = unknown, Context = unknown> {
|
||||||
__blitz: true
|
__blitz: true
|
||||||
@@ -9,49 +15,84 @@ interface ResultWithContext<Result = unknown, Context = unknown> {
|
|||||||
}
|
}
|
||||||
function isResultWithContext(x: unknown): x is ResultWithContext {
|
function isResultWithContext(x: unknown): x is ResultWithContext {
|
||||||
return (
|
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
|
session: AuthenticatedSessionContext
|
||||||
}
|
}
|
||||||
|
|
||||||
type PipeFn<Prev, Next, PrevCtx, NextCtx = PrevCtx> = (
|
type PipeFn<Prev, Next, PrevCtx, NextCtx = PrevCtx> = (
|
||||||
i: Await<Prev>,
|
i: Await<Prev>,
|
||||||
c: PrevCtx,
|
c: PrevCtx
|
||||||
) => Next extends ResultWithContext ? never : Next | ResultWithContext<Next, NextCtx>
|
) => 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>(
|
function pipe<A, B, C, CA = Ctx, CB = CA, CC = CB>(
|
||||||
ab: PipeFn<A, B, CA, 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>
|
): (input: A, ctx: CA) => EnsurePromise<C>
|
||||||
function pipe<A, B, C, D, CA = Ctx, CB = CA, CC = CB, CD = CC>(
|
function pipe<A, B, C, D, CA = Ctx, CB = CA, CC = CB, CD = CC>(
|
||||||
ab: PipeFn<A, B, CA, CB>,
|
ab: PipeFn<A, B, CA, CB>,
|
||||||
bc: PipeFn<B, C, CB, CC>,
|
bc: PipeFn<B, C, CB, CC>,
|
||||||
cd: PipeFn<C, D, CC, CD>,
|
cd: PipeFn<C, D, CC, CD>
|
||||||
): (input: A, ctx: CA) => EnsurePromise<D>
|
): (input: A, ctx: CA) => EnsurePromise<D>
|
||||||
function pipe<A, B, C, D, E, CA = Ctx, CB = CA, CC = CB, CD = CC, CE = CD>(
|
function pipe<A, B, C, D, E, CA = Ctx, CB = CA, CC = CB, CD = CC, CE = CD>(
|
||||||
ab: PipeFn<A, B, CA, CB>,
|
ab: PipeFn<A, B, CA, CB>,
|
||||||
bc: PipeFn<B, C, CB, CC>,
|
bc: PipeFn<B, C, CB, CC>,
|
||||||
cd: PipeFn<C, D, CC, CD>,
|
cd: PipeFn<C, D, CC, CD>,
|
||||||
de: PipeFn<D, E, CD, CE>,
|
de: PipeFn<D, E, CD, CE>
|
||||||
): (input: A, ctx: CA) => EnsurePromise<E>
|
): (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>,
|
ab: PipeFn<A, B, CA, CB>,
|
||||||
bc: PipeFn<B, C, CB, CC>,
|
bc: PipeFn<B, C, CB, CC>,
|
||||||
cd: PipeFn<C, D, CC, CD>,
|
cd: PipeFn<C, D, CC, CD>,
|
||||||
de: PipeFn<D, E, CD, CE>,
|
de: PipeFn<D, E, CD, CE>,
|
||||||
ef: PipeFn<E, F, CE, CF>,
|
ef: PipeFn<E, F, CE, CF>
|
||||||
): (input: A, ctx: CA) => EnsurePromise<F>
|
): (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>,
|
ab: PipeFn<A, B, CA, CB>,
|
||||||
bc: PipeFn<B, C, CB, CC>,
|
bc: PipeFn<B, C, CB, CC>,
|
||||||
cd: PipeFn<C, D, CC, CD>,
|
cd: PipeFn<C, D, CC, CD>,
|
||||||
de: PipeFn<D, E, CD, CE>,
|
de: PipeFn<D, E, CD, CE>,
|
||||||
ef: PipeFn<E, F, CE, CF>,
|
ef: PipeFn<E, F, CE, CF>,
|
||||||
fg: PipeFn<F, G, CF, CG>,
|
fg: PipeFn<F, G, CF, CG>
|
||||||
): (input: A, ctx: CA) => EnsurePromise<CG>
|
): (input: A, ctx: CA) => EnsurePromise<CG>
|
||||||
function pipe<
|
function pipe<
|
||||||
A,
|
A,
|
||||||
@@ -77,7 +118,7 @@ function pipe<
|
|||||||
de: PipeFn<D, E, CD, CE>,
|
de: PipeFn<D, E, CD, CE>,
|
||||||
ef: PipeFn<E, F, CE, CF>,
|
ef: PipeFn<E, F, CE, CF>,
|
||||||
fg: PipeFn<F, G, CF, CG>,
|
fg: PipeFn<F, G, CF, CG>,
|
||||||
gh: PipeFn<G, H, CG, CH>,
|
gh: PipeFn<G, H, CG, CH>
|
||||||
): (input: A, ctx: CA) => EnsurePromise<H>
|
): (input: A, ctx: CA) => EnsurePromise<H>
|
||||||
function pipe<
|
function pipe<
|
||||||
A,
|
A,
|
||||||
@@ -106,7 +147,7 @@ function pipe<
|
|||||||
ef: PipeFn<E, F, CE, CF>,
|
ef: PipeFn<E, F, CE, CF>,
|
||||||
fg: PipeFn<F, G, CF, CG>,
|
fg: PipeFn<F, G, CF, CG>,
|
||||||
gh: PipeFn<G, H, CG, CH>,
|
gh: PipeFn<G, H, CG, CH>,
|
||||||
hi: PipeFn<H, I, CH, CI>,
|
hi: PipeFn<H, I, CH, CI>
|
||||||
): (input: A, ctx: CA) => EnsurePromise<I>
|
): (input: A, ctx: CA) => EnsurePromise<I>
|
||||||
function pipe<
|
function pipe<
|
||||||
A,
|
A,
|
||||||
@@ -138,7 +179,7 @@ function pipe<
|
|||||||
fg: PipeFn<F, G, CF, CG>,
|
fg: PipeFn<F, G, CF, CG>,
|
||||||
gh: PipeFn<G, H, CG, CH>,
|
gh: PipeFn<G, H, CG, CH>,
|
||||||
hi: PipeFn<H, I, CH, CI>,
|
hi: PipeFn<H, I, CH, CI>,
|
||||||
ij: PipeFn<I, J, CI, CJ>,
|
ij: PipeFn<I, J, CI, CJ>
|
||||||
): (input: A, ctx: CA) => EnsurePromise<J>
|
): (input: A, ctx: CA) => EnsurePromise<J>
|
||||||
function pipe<
|
function pipe<
|
||||||
A,
|
A,
|
||||||
@@ -173,7 +214,7 @@ function pipe<
|
|||||||
gh: PipeFn<G, H, CG, CH>,
|
gh: PipeFn<G, H, CG, CH>,
|
||||||
hi: PipeFn<H, I, CH, CI>,
|
hi: PipeFn<H, I, CH, CI>,
|
||||||
ij: PipeFn<I, J, CI, CJ>,
|
ij: PipeFn<I, J, CI, CJ>,
|
||||||
jk: PipeFn<J, K, CJ, CK>,
|
jk: PipeFn<J, K, CJ, CK>
|
||||||
): (input: A, ctx: CA) => EnsurePromise<K>
|
): (input: A, ctx: CA) => EnsurePromise<K>
|
||||||
function pipe<
|
function pipe<
|
||||||
A,
|
A,
|
||||||
@@ -211,7 +252,7 @@ function pipe<
|
|||||||
hi: PipeFn<H, I, CH, CI>,
|
hi: PipeFn<H, I, CH, CI>,
|
||||||
ij: PipeFn<I, J, CI, CJ>,
|
ij: PipeFn<I, J, CI, CJ>,
|
||||||
jk: PipeFn<J, K, CJ, CK>,
|
jk: PipeFn<J, K, CJ, CK>,
|
||||||
kl: PipeFn<K, L, CK, CL>,
|
kl: PipeFn<K, L, CK, CL>
|
||||||
): (input: A, ctx: CA) => EnsurePromise<L>
|
): (input: A, ctx: CA) => EnsurePromise<L>
|
||||||
function pipe<
|
function pipe<
|
||||||
A,
|
A,
|
||||||
@@ -252,7 +293,7 @@ function pipe<
|
|||||||
ij: PipeFn<I, J, CI, CJ>,
|
ij: PipeFn<I, J, CI, CJ>,
|
||||||
jk: PipeFn<J, K, CJ, CK>,
|
jk: PipeFn<J, K, CJ, CK>,
|
||||||
kl: PipeFn<K, L, CK, CL>,
|
kl: PipeFn<K, L, CK, CL>,
|
||||||
lm: PipeFn<L, M, CL, CM>,
|
lm: PipeFn<L, M, CL, CM>
|
||||||
): (input: A, ctx: CA) => EnsurePromise<M>
|
): (input: A, ctx: CA) => EnsurePromise<M>
|
||||||
function pipe(...args: unknown[]): unknown {
|
function pipe(...args: unknown[]): unknown {
|
||||||
const functions = args as PipeFn<unknown, unknown, Ctx>[]
|
const functions = args as PipeFn<unknown, unknown, Ctx>[]
|
||||||
@@ -271,9 +312,9 @@ function pipe(...args: unknown[]): unknown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ResolverAuthorize {
|
interface ResolverAuthorize {
|
||||||
<T, C = Ctx>(...args: Parameters<SessionContextBase["$authorize"]>): (
|
<T, C = Ctx>(...args: Parameters<SessionContextBase['$authorize']>): (
|
||||||
input: T,
|
input: T,
|
||||||
ctx: C,
|
ctx: C
|
||||||
) => ResultWithContext<T, AuthenticatedMiddlewareCtx>
|
) => ResultWithContext<T, AuthenticatedMiddlewareCtx>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,24 +331,30 @@ const authorize: ResolverAuthorize = (...args) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ParserType = "sync" | "async"
|
function zod<
|
||||||
|
Schema extends ZodTypeAny,
|
||||||
function zod<Schema extends ZodTypeAny, InputType = zInput<Schema>, OutputType = zOutput<Schema>>(
|
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,
|
schema: Schema,
|
||||||
parserType: "sync",
|
parserType: 'async'
|
||||||
): (input: InputType) => OutputType
|
|
||||||
function zod<Schema extends ZodTypeAny, InputType = zInput<Schema>, OutputType = zOutput<Schema>>(
|
|
||||||
schema: Schema,
|
|
||||||
parserType: "async",
|
|
||||||
): (input: InputType) => Promise<OutputType>
|
): (input: InputType) => Promise<OutputType>
|
||||||
function zod<Schema extends ZodTypeAny, InputType = zInput<Schema>, OutputType = zOutput<Schema>>(
|
function zod<
|
||||||
schema: Schema,
|
Schema extends ZodTypeAny,
|
||||||
): (input: InputType) => Promise<OutputType>
|
InputType = zInput<Schema>,
|
||||||
function zod<Schema extends ZodTypeAny, InputType = zInput<Schema>, OutputType = zOutput<Schema>>(
|
OutputType = zOutput<Schema>
|
||||||
schema: Schema,
|
>(schema: Schema): (input: InputType) => Promise<OutputType>
|
||||||
parserType: ParserType = "async",
|
function zod<
|
||||||
) {
|
Schema extends ZodTypeAny,
|
||||||
if (parserType === "sync") {
|
InputType = zInput<Schema>,
|
||||||
|
OutputType = zOutput<Schema>
|
||||||
|
>(schema: Schema, parserType: ParserType = 'async') {
|
||||||
|
if (parserType === 'sync') {
|
||||||
return (input: InputType): OutputType => schema.parse(input)
|
return (input: InputType): OutputType => schema.parse(input)
|
||||||
} else {
|
} else {
|
||||||
return (input: InputType): Promise<OutputType> => schema.parseAsync(input)
|
return (input: InputType): Promise<OutputType> => schema.parseAsync(input)
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
import {getPublicDataStore, useAuthorizeIf, useSession} from "next/data-client"
|
import {
|
||||||
import {BlitzProvider} from "next/data-client"
|
getPublicDataStore,
|
||||||
import {formatWithValidation} from "next/dist/shared/lib/utils"
|
useAuthorizeIf,
|
||||||
import {RedirectError} from "next/stdlib"
|
useSession,
|
||||||
import {AppProps, BlitzPage} from "next/types"
|
} from '../data-client/auth'
|
||||||
import React, {ComponentPropsWithoutRef, useEffect} from "react"
|
import { BlitzProvider } from '../data-client/react-query'
|
||||||
import SuperJSON from "superjson"
|
import { formatWithValidation } from '../shared/lib/utils'
|
||||||
import {Head} from "./head"
|
import { Head } from '../shared/lib/head'
|
||||||
import {clientDebug} from "./utils"
|
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 = `
|
const customCSS = `
|
||||||
body::before {
|
body::before {
|
||||||
@@ -34,15 +38,18 @@ const noscriptCSS = `
|
|||||||
const NoPageFlicker = () => {
|
const NoPageFlicker = () => {
|
||||||
return (
|
return (
|
||||||
<Head>
|
<Head>
|
||||||
<style dangerouslySetInnerHTML={{__html: customCSS}} />
|
<style dangerouslySetInnerHTML={{ __html: customCSS }} />
|
||||||
<noscript>
|
<noscript>
|
||||||
<style dangerouslySetInnerHTML={{__html: noscriptCSS}} />
|
<style dangerouslySetInnerHTML={{ __html: noscriptCSS }} />
|
||||||
</noscript>
|
</noscript>
|
||||||
</Head>
|
</Head>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAuthValues(Page: BlitzPage, props: ComponentPropsWithoutRef<BlitzPage>) {
|
function getAuthValues(
|
||||||
|
Page: BlitzPage,
|
||||||
|
props: ComponentPropsWithoutRef<BlitzPage>
|
||||||
|
) {
|
||||||
let authenticate = Page.authenticate
|
let authenticate = Page.authenticate
|
||||||
let redirectAuthenticatedTo = Page.redirectAuthenticatedTo
|
let redirectAuthenticatedTo = Page.redirectAuthenticatedTo
|
||||||
|
|
||||||
@@ -54,7 +61,10 @@ function getAuthValues(Page: BlitzPage, props: ComponentPropsWithoutRef<BlitzPag
|
|||||||
while (true) {
|
while (true) {
|
||||||
const type = layout.type
|
const type = layout.type
|
||||||
|
|
||||||
if (type.authenticate !== undefined || type.redirectAuthenticatedTo !== undefined) {
|
if (
|
||||||
|
type.authenticate !== undefined ||
|
||||||
|
type.redirectAuthenticatedTo !== undefined
|
||||||
|
) {
|
||||||
authenticate = type.authenticate
|
authenticate = type.authenticate
|
||||||
redirectAuthenticatedTo = type.redirectAuthenticatedTo
|
redirectAuthenticatedTo = type.redirectAuthenticatedTo
|
||||||
break
|
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>) => {
|
const BlitzInnerRoot = (props: ComponentPropsWithoutRef<BlitzPage>) => {
|
||||||
// We call useSession so this will rerender anytime session changes
|
// 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)
|
useAuthorizeIf(authenticate === true)
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== 'undefined') {
|
||||||
const publicData = getPublicDataStore().getData()
|
const publicData = getPublicDataStore().getData()
|
||||||
// We read directly from publicData.userId instead of useSession
|
// We read directly from publicData.userId instead of useSession
|
||||||
// so we can access userId on first render. useSession is always empty on first render
|
// so we can access userId on first render. useSession is always empty on first render
|
||||||
if (publicData.userId) {
|
if (publicData.userId) {
|
||||||
clientDebug("[BlitzInnerRoot] logged in")
|
debug('[BlitzInnerRoot] logged in')
|
||||||
|
|
||||||
if (typeof redirectAuthenticatedTo === "function") {
|
if (typeof redirectAuthenticatedTo === 'function') {
|
||||||
redirectAuthenticatedTo = redirectAuthenticatedTo({session: publicData})
|
redirectAuthenticatedTo = redirectAuthenticatedTo({
|
||||||
|
session: publicData,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redirectAuthenticatedTo) {
|
if (redirectAuthenticatedTo) {
|
||||||
const redirectUrl =
|
const redirectUrl =
|
||||||
typeof redirectAuthenticatedTo === "string"
|
typeof redirectAuthenticatedTo === 'string'
|
||||||
? redirectAuthenticatedTo
|
? redirectAuthenticatedTo
|
||||||
: formatWithValidation(redirectAuthenticatedTo)
|
: formatWithValidation(redirectAuthenticatedTo)
|
||||||
|
|
||||||
clientDebug("[BlitzInnerRoot] redirecting to", redirectUrl)
|
debug('[BlitzInnerRoot] redirecting to', redirectUrl)
|
||||||
const error = new RedirectError(redirectUrl)
|
const error = new RedirectError(redirectUrl)
|
||||||
error.stack = null!
|
error.stack = null!
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
clientDebug("[BlitzInnerRoot] logged out")
|
debug('[BlitzInnerRoot] logged out')
|
||||||
if (authenticate && typeof authenticate === "object" && authenticate.redirectTo) {
|
if (
|
||||||
let {redirectTo} = authenticate
|
authenticate &&
|
||||||
if (typeof redirectTo !== "string") {
|
typeof authenticate === 'object' &&
|
||||||
|
authenticate.redirectTo
|
||||||
|
) {
|
||||||
|
let { redirectTo } = authenticate
|
||||||
|
if (typeof redirectTo !== 'string') {
|
||||||
redirectTo = formatWithValidation(redirectTo)
|
redirectTo = formatWithValidation(redirectTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(redirectTo, window.location.href)
|
const url = new URL(redirectTo, window.location.href)
|
||||||
url.searchParams.append("next", window.location.pathname)
|
url.searchParams.append('next', window.location.pathname)
|
||||||
clientDebug("[BlitzInnerRoot] redirecting to", url.toString())
|
debug('[BlitzInnerRoot] redirecting to', url.toString())
|
||||||
const error = new RedirectError(url.toString())
|
const error = new RedirectError(url.toString())
|
||||||
error.stack = null!
|
error.stack = null!
|
||||||
throw error
|
throw error
|
||||||
@@ -126,7 +142,7 @@ export function withBlitzInnerWrapper(Page: BlitzPage) {
|
|||||||
for (let [key, value] of Object.entries(Page)) {
|
for (let [key, value] of Object.entries(Page)) {
|
||||||
;(BlitzInnerRoot as any)[key] = value
|
;(BlitzInnerRoot as any)[key] = value
|
||||||
}
|
}
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
BlitzInnerRoot.displayName = `BlitzInnerRoot`
|
BlitzInnerRoot.displayName = `BlitzInnerRoot`
|
||||||
}
|
}
|
||||||
return BlitzInnerRoot
|
return BlitzInnerRoot
|
||||||
@@ -134,9 +150,15 @@ export function withBlitzInnerWrapper(Page: BlitzPage) {
|
|||||||
|
|
||||||
export function withBlitzAppRoot(UserAppRoot: React.ComponentType<any>) {
|
export function withBlitzAppRoot(UserAppRoot: React.ComponentType<any>) {
|
||||||
const BlitzOuterRoot = (props: AppProps) => {
|
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 =
|
const noPageFlicker =
|
||||||
props.Component.suppressFirstRenderFlicker ||
|
props.Component.suppressFirstRenderFlicker ||
|
||||||
@@ -144,15 +166,15 @@ export function withBlitzAppRoot(UserAppRoot: React.ComponentType<any>) {
|
|||||||
redirectAuthenticatedTo
|
redirectAuthenticatedTo
|
||||||
|
|
||||||
useEffect(() => {
|
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) {
|
if (dehydratedState && _superjson) {
|
||||||
const deserializedProps = SuperJSON.deserialize({
|
const deserializedProps = SuperJSON.deserialize({
|
||||||
json: {dehydratedState},
|
json: { dehydratedState },
|
||||||
meta: _superjson,
|
meta: _superjson,
|
||||||
}) as {dehydratedState: any}
|
}) as { dehydratedState: any }
|
||||||
dehydratedState = deserializedProps?.dehydratedState
|
dehydratedState = deserializedProps?.dehydratedState
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,42 +1,44 @@
|
|||||||
import {Router} from "next/router"
|
import { Router } from '../client/router'
|
||||||
import {RedirectError} from "next/stdlib"
|
import { RedirectError } from './errors'
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import {RouterContext} from "./router"
|
import { RouterContext } from '../shared/lib/router-context'
|
||||||
import {clientDebug} from "./utils"
|
const debug = require('debug')('blitz:errorboundary')
|
||||||
|
|
||||||
const changedArray = (a: Array<unknown> = [], b: Array<unknown> = []) =>
|
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]))
|
a.length !== b.length || a.some((item, index) => !Object.is(item, b[index]))
|
||||||
|
|
||||||
interface FallbackProps {
|
interface ErrorFallbackProps {
|
||||||
error: Error
|
error: Error & Record<any, any>
|
||||||
resetErrorBoundary: (...args: Array<unknown>) => void
|
resetErrorBoundary: (...args: Array<unknown>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorBoundaryPropsWithComponent {
|
interface ErrorBoundaryPropsWithComponent {
|
||||||
onResetKeysChange?: (
|
onResetKeysChange?: (
|
||||||
prevResetKeys: Array<unknown> | undefined,
|
prevResetKeys: Array<unknown> | undefined,
|
||||||
resetKeys: Array<unknown> | undefined,
|
resetKeys: Array<unknown> | undefined
|
||||||
) => void
|
) => void
|
||||||
onReset?: (...args: Array<unknown>) => void
|
onReset?: (...args: Array<unknown>) => void
|
||||||
onError?: (error: Error, info: {componentStack: string}) => void
|
onError?: (error: Error, info: { componentStack: string }) => void
|
||||||
resetKeys?: Array<unknown>
|
resetKeys?: Array<unknown>
|
||||||
fallback?: never
|
fallback?: never
|
||||||
FallbackComponent: React.ComponentType<FallbackProps>
|
FallbackComponent: React.ComponentType<ErrorFallbackProps>
|
||||||
fallbackRender?: never
|
fallbackRender?: never
|
||||||
}
|
}
|
||||||
|
|
||||||
declare function FallbackRender(
|
declare function FallbackRender(
|
||||||
props: FallbackProps,
|
props: ErrorFallbackProps
|
||||||
): React.ReactElement<unknown, string | React.FunctionComponent | typeof React.Component> | null
|
): React.ReactElement<
|
||||||
|
unknown,
|
||||||
|
string | React.FunctionComponent | typeof React.Component
|
||||||
|
> | null
|
||||||
|
|
||||||
interface ErrorBoundaryPropsWithRender {
|
interface ErrorBoundaryPropsWithRender {
|
||||||
onResetKeysChange?: (
|
onResetKeysChange?: (
|
||||||
prevResetKeys: Array<unknown> | undefined,
|
prevResetKeys: Array<unknown> | undefined,
|
||||||
resetKeys: Array<unknown> | undefined,
|
resetKeys: Array<unknown> | undefined
|
||||||
) => void
|
) => void
|
||||||
onReset?: (...args: Array<unknown>) => void
|
onReset?: (...args: Array<unknown>) => void
|
||||||
onError?: (error: Error, info: {componentStack: string}) => void
|
onError?: (error: Error, info: { componentStack: string }) => void
|
||||||
resetKeys?: Array<unknown>
|
resetKeys?: Array<unknown>
|
||||||
fallback?: never
|
fallback?: never
|
||||||
FallbackComponent?: never
|
FallbackComponent?: never
|
||||||
@@ -46,10 +48,10 @@ interface ErrorBoundaryPropsWithRender {
|
|||||||
interface ErrorBoundaryPropsWithFallback {
|
interface ErrorBoundaryPropsWithFallback {
|
||||||
onResetKeysChange?: (
|
onResetKeysChange?: (
|
||||||
prevResetKeys: Array<unknown> | undefined,
|
prevResetKeys: Array<unknown> | undefined,
|
||||||
resetKeys: Array<unknown> | undefined,
|
resetKeys: Array<unknown> | undefined
|
||||||
) => void
|
) => void
|
||||||
onReset?: (...args: Array<unknown>) => void
|
onReset?: (...args: Array<unknown>) => void
|
||||||
onError?: (error: Error, info: {componentStack: string}) => void
|
onError?: (error: Error, info: { componentStack: string }) => void
|
||||||
resetKeys?: Array<unknown>
|
resetKeys?: Array<unknown>
|
||||||
fallback: React.ReactElement<
|
fallback: React.ReactElement<
|
||||||
unknown,
|
unknown,
|
||||||
@@ -64,9 +66,9 @@ type ErrorBoundaryProps =
|
|||||||
| ErrorBoundaryPropsWithComponent
|
| ErrorBoundaryPropsWithComponent
|
||||||
| ErrorBoundaryPropsWithRender
|
| 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<
|
class ErrorBoundary extends React.Component<
|
||||||
React.PropsWithRef<React.PropsWithChildren<ErrorBoundaryProps>>,
|
React.PropsWithRef<React.PropsWithChildren<ErrorBoundaryProps>>,
|
||||||
@@ -75,7 +77,7 @@ class ErrorBoundary extends React.Component<
|
|||||||
static contextType = RouterContext
|
static contextType = RouterContext
|
||||||
|
|
||||||
static getDerivedStateFromError(error: Error) {
|
static getDerivedStateFromError(error: Error) {
|
||||||
return {error}
|
return { error }
|
||||||
}
|
}
|
||||||
|
|
||||||
state = initialState
|
state = initialState
|
||||||
@@ -92,7 +94,7 @@ class ErrorBoundary extends React.Component<
|
|||||||
|
|
||||||
async componentDidCatch(error: Error, info: React.ErrorInfo) {
|
async componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||||
if (error instanceof RedirectError) {
|
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)
|
await (this.context as Router)?.push(error.url)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -100,29 +102,35 @@ class ErrorBoundary extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {error} = this.state
|
const { error } = this.state
|
||||||
|
|
||||||
if (error !== null) {
|
if (error !== null) {
|
||||||
this.updatedWithError = true
|
this.updatedWithError = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically reset on route change
|
// Automatically reset on route change
|
||||||
;(this.context as Router)?.events?.on("routeChangeComplete", this.handleRouteChange)
|
;(this.context as Router)?.events?.on(
|
||||||
|
'routeChangeComplete',
|
||||||
|
this.handleRouteChange
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRouteChange = () => {
|
handleRouteChange = () => {
|
||||||
clientDebug("Resetting error boundary on route change")
|
debug('Resetting error boundary on route change')
|
||||||
this.props.onReset?.()
|
this.props.onReset?.()
|
||||||
this.reset()
|
this.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
;(this.context as Router)?.events?.off("routeChangeComplete", this.handleRouteChange)
|
;(this.context as Router)?.events?.off(
|
||||||
|
'routeChangeComplete',
|
||||||
|
this.handleRouteChange
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: ErrorBoundaryProps) {
|
componentDidUpdate(prevProps: ErrorBoundaryProps) {
|
||||||
const {error} = this.state
|
const { error } = this.state
|
||||||
const {resetKeys} = this.props
|
const { resetKeys } = this.props
|
||||||
|
|
||||||
// There's an edge case where if the thing that triggered the error
|
// 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
|
// happens to *also* be in the resetKeys array, we'd end up resetting
|
||||||
@@ -142,9 +150,9 @@ class ErrorBoundary extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {error} = this.state
|
const { error } = this.state
|
||||||
|
|
||||||
const {fallbackRender, FallbackComponent, fallback} = this.props
|
const { fallbackRender, FallbackComponent, fallback } = this.props
|
||||||
|
|
||||||
if (error !== null) {
|
if (error !== null) {
|
||||||
const props = {
|
const props = {
|
||||||
@@ -156,13 +164,13 @@ class ErrorBoundary extends React.Component<
|
|||||||
return null
|
return null
|
||||||
} else if (React.isValidElement(fallback)) {
|
} else if (React.isValidElement(fallback)) {
|
||||||
return fallback
|
return fallback
|
||||||
} else if (typeof fallbackRender === "function") {
|
} else if (typeof fallbackRender === 'function') {
|
||||||
return fallbackRender(props)
|
return fallbackRender(props)
|
||||||
} else if (FallbackComponent) {
|
} else if (FallbackComponent) {
|
||||||
return <FallbackComponent {...props} />
|
return <FallbackComponent {...props} />
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
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>(
|
function withErrorBoundary<P>(
|
||||||
Component: React.ComponentType<P>,
|
Component: React.ComponentType<P>,
|
||||||
errorBoundaryProps: ErrorBoundaryProps,
|
errorBoundaryProps: ErrorBoundaryProps
|
||||||
): React.ComponentType<P> {
|
): React.ComponentType<P> {
|
||||||
const Wrapped: React.ComponentType<P> = (props) => {
|
const Wrapped: React.ComponentType<P> = (props) => {
|
||||||
return (
|
return (
|
||||||
@@ -184,7 +192,7 @@ function withErrorBoundary<P>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Format for display in DevTools
|
// Format for display in DevTools
|
||||||
const name = Component.displayName || Component.name || "Unknown"
|
const name = Component.displayName || Component.name || 'Unknown'
|
||||||
Wrapped.displayName = `withErrorBoundary(${name})`
|
Wrapped.displayName = `withErrorBoundary(${name})`
|
||||||
|
|
||||||
return Wrapped
|
return Wrapped
|
||||||
@@ -197,9 +205,9 @@ function useErrorHandler(givenError?: unknown): (error: unknown) => void {
|
|||||||
return setError
|
return setError
|
||||||
}
|
}
|
||||||
|
|
||||||
export {ErrorBoundary, withErrorBoundary, useErrorHandler}
|
export { ErrorBoundary, withErrorBoundary, useErrorHandler }
|
||||||
export type {
|
export type {
|
||||||
FallbackProps,
|
ErrorFallbackProps,
|
||||||
ErrorBoundaryPropsWithComponent,
|
ErrorBoundaryPropsWithComponent,
|
||||||
ErrorBoundaryPropsWithRender,
|
ErrorBoundaryPropsWithRender,
|
||||||
ErrorBoundaryPropsWithFallback,
|
ErrorBoundaryPropsWithFallback,
|
||||||
@@ -1,4 +1,21 @@
|
|||||||
|
export { Routes } from '.blitz'
|
||||||
export * from './errors'
|
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 isServer = typeof window === 'undefined'
|
||||||
export const isClient = typeof window !== 'undefined'
|
export const isClient = typeof window !== 'undefined'
|
||||||
|
|||||||
66
nextjs/packages/next/stdlib/prisma-utils.ts
Normal file
66
nextjs/packages/next/stdlib/prisma-utils.ts
Normal 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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,18 +1,11 @@
|
|||||||
import {ZodError} from "zod"
|
import { ParserType } from '../types/index'
|
||||||
import {ParserType} from "../server/resolver"
|
import { ZodError } from 'zod'
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatZodError(error: ZodError) {
|
export function formatZodError(error: ZodError) {
|
||||||
if (!error || typeof error.format !== "function") {
|
if (!error || typeof error.format !== 'function') {
|
||||||
throw new Error("The argument to formatZodError must be a zod error with error.format()")
|
throw new Error(
|
||||||
|
'The argument to formatZodError must be a zod error with error.format()'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors = error.format()
|
const errors = error.format()
|
||||||
@@ -23,7 +16,7 @@ export function recursiveFormatZodErrors(errors: any) {
|
|||||||
let formattedErrors: Record<string, any> = {}
|
let formattedErrors: Record<string, any> = {}
|
||||||
|
|
||||||
for (const key in errors) {
|
for (const key in errors) {
|
||||||
if (key === "_errors") {
|
if (key === '_errors') {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,11 +58,20 @@ const validateZodSchemaAsync = (schema: any) => async (values: any) => {
|
|||||||
|
|
||||||
// type zodSchemaReturn = typeof validateZodSchemaAsync | typeof validateZodSchemaSync
|
// type zodSchemaReturn = typeof validateZodSchemaAsync | typeof validateZodSchemaSync
|
||||||
// : (((values:any) => any) | ((values:any) => Promise<any>)) =>
|
// : (((values:any) => any) | ((values:any) => Promise<any>)) =>
|
||||||
export function validateZodSchema(schema: any, parserType: "sync"): (values: any) => any
|
export function validateZodSchema(
|
||||||
export function validateZodSchema(schema: any, parserType: "async"): (values: any) => Promise<any>
|
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): (values: any) => Promise<any>
|
||||||
export function validateZodSchema(schema: any, parserType: ParserType = "async") {
|
export function validateZodSchema(
|
||||||
if (parserType === "sync") {
|
schema: any,
|
||||||
|
parserType: ParserType = 'async'
|
||||||
|
) {
|
||||||
|
if (parserType === 'sync') {
|
||||||
return validateZodSchemaSync(schema)
|
return validateZodSchemaSync(schema)
|
||||||
} else {
|
} else {
|
||||||
return validateZodSchemaAsync(schema)
|
return validateZodSchemaAsync(schema)
|
||||||
@@ -405,6 +405,16 @@ export async function ncc_lodash_curry(task, opts) {
|
|||||||
.target('compiled/lodash.curry')
|
.target('compiled/lodash.curry')
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line camelcase
|
// 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'
|
externals['lru-cache'] = 'next/dist/compiled/lru-cache'
|
||||||
export async function ncc_lru_cache(task, opts) {
|
export async function ncc_lru_cache(task, opts) {
|
||||||
await task
|
await task
|
||||||
@@ -781,6 +791,7 @@ export async function ncc(task, opts) {
|
|||||||
'ncc_jsonwebtoken',
|
'ncc_jsonwebtoken',
|
||||||
'ncc_loader_utils',
|
'ncc_loader_utils',
|
||||||
'ncc_lodash_curry',
|
'ncc_lodash_curry',
|
||||||
|
'ncc_lodash_frompairs',
|
||||||
'ncc_lru_cache',
|
'ncc_lru_cache',
|
||||||
'ncc_nanoid',
|
'ncc_nanoid',
|
||||||
'ncc_neo_async',
|
'ncc_neo_async',
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"useUnknownInCatchVariables": false,
|
"useUnknownInCatchVariables": false,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"strictPropertyInitialization": true,
|
||||||
"jsx": "react"
|
"jsx": "react"
|
||||||
},
|
},
|
||||||
"exclude": ["dist", "./*.d.ts"]
|
"exclude": ["dist", "./*.d.ts"]
|
||||||
|
|||||||
4
nextjs/packages/next/types/global.d.ts
vendored
4
nextjs/packages/next/types/global.d.ts
vendored
@@ -9,6 +9,10 @@ declare namespace NodeJS {
|
|||||||
interface ProcessEnv {
|
interface ProcessEnv {
|
||||||
readonly NODE_ENV: 'development' | 'production' | 'test'
|
readonly NODE_ENV: 'development' | 'production' | 'test'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Global {
|
||||||
|
_blitz_prismaClient: any
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '*.module.css' {
|
declare module '*.module.css' {
|
||||||
|
|||||||
2
nextjs/packages/next/types/index.d.ts
vendored
2
nextjs/packages/next/types/index.d.ts
vendored
@@ -262,3 +262,5 @@ export type InferGetServerSidePropsType<T> = T extends GetServerSideProps<
|
|||||||
) => Promise<GetServerSidePropsResult<infer P>>
|
) => Promise<GetServerSidePropsResult<infer P>>
|
||||||
? P
|
? P
|
||||||
: never
|
: never
|
||||||
|
|
||||||
|
export type ParserType = 'sync' | 'async'
|
||||||
|
|||||||
48
nextjs/packages/next/types/misc.d.ts
vendored
48
nextjs/packages/next/types/misc.d.ts
vendored
@@ -143,49 +143,13 @@ declare module 'next/dist/compiled/jsonwebtoken' {
|
|||||||
import m from 'jsonwebtoken'
|
import m from 'jsonwebtoken'
|
||||||
export = m
|
export = m
|
||||||
}
|
}
|
||||||
|
declare module 'next/dist/compiled/lodash.frompairs' {
|
||||||
|
import m from 'lodash.frompairs'
|
||||||
|
export = m
|
||||||
|
}
|
||||||
declare module 'next/dist/compiled/lodash.curry' {
|
declare module 'next/dist/compiled/lodash.curry' {
|
||||||
// import m from 'lodash.curry'
|
import m from 'lodash.curry'
|
||||||
// export = m
|
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
|
|
||||||
}
|
}
|
||||||
declare module 'next/dist/compiled/lru-cache' {
|
declare module 'next/dist/compiled/lru-cache' {
|
||||||
import m from 'lru-cache'
|
import m from 'lru-cache'
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {render, screen} from "@testing-library/react"
|
import { render, screen } from '@testing-library/react'
|
||||||
import userEvent from "@testing-library/user-event"
|
import userEvent from '@testing-library/user-event'
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import type {FallbackProps} from "./error-boundary"
|
import type { ErrorFallbackProps } from 'next/stdlib'
|
||||||
import {ErrorBoundary, useErrorHandler} from "./error-boundary"
|
import { ErrorBoundary, useErrorHandler } from 'next/stdlib'
|
||||||
import {cleanStack} from "./error-boundary.test"
|
import { cleanStack } from './error-boundary.unit.test'
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.resetAllMocks()
|
jest.resetAllMocks()
|
||||||
@@ -11,7 +11,7 @@ afterEach(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(console, "error").mockImplementation(() => {})
|
jest.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
// afterEach(() => {
|
// afterEach(() => {
|
||||||
@@ -24,7 +24,7 @@ beforeEach(() => {
|
|||||||
// }
|
// }
|
||||||
// })
|
// })
|
||||||
|
|
||||||
function ErrorFallback({error, resetErrorBoundary}: FallbackProps) {
|
function ErrorFallback({ error, resetErrorBoundary }: ErrorFallbackProps) {
|
||||||
return (
|
return (
|
||||||
<div role="alert">
|
<div role="alert">
|
||||||
<p>Something went wrong:</p>
|
<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() {
|
function AsyncBomb() {
|
||||||
const [explode, setExplode] = React.useState(false)
|
const [explode, setExplode] = React.useState(false)
|
||||||
const handleError = useErrorHandler()
|
const handleError = useErrorHandler()
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (explode) {
|
if (explode) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
handleError(new Error("💥 CABOOM 💥"))
|
handleError(new Error('💥 CABOOM 💥'))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -52,17 +52,19 @@ test("handleError forwards along async errors", async () => {
|
|||||||
render(
|
render(
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
<AsyncBomb />
|
<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 consoleError = console.error as jest.Mock<void, unknown[]>
|
||||||
const [[actualError], [componentStack]] = consoleError.mock.calls
|
const [[actualError], [componentStack]] = consoleError.mock.calls
|
||||||
const firstLineOfError = firstLine(actualError as string)
|
const firstLineOfError = firstLine(actualError as string)
|
||||||
expect(firstLineOfError).toMatchInlineSnapshot(`"Error: Uncaught [Error: 💥 CABOOM 💥]"`)
|
expect(firstLineOfError).toMatchInlineSnapshot(
|
||||||
|
`"Error: Uncaught [Error: 💥 CABOOM 💥]"`
|
||||||
|
)
|
||||||
expect(cleanStack(componentStack)).toMatchInlineSnapshot(`
|
expect(cleanStack(componentStack)).toMatchInlineSnapshot(`
|
||||||
"The above error occurred in the <AsyncBomb> component:
|
"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()
|
consoleError.mockClear()
|
||||||
|
|
||||||
// can recover
|
// 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()
|
expect(console.error).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("can pass an error to useErrorHandler", async () => {
|
test('can pass an error to useErrorHandler', async () => {
|
||||||
function AsyncBomb() {
|
function AsyncBomb() {
|
||||||
const [error, setError] = React.useState<Error | null>(null)
|
const [error, setError] = React.useState<Error | null>(null)
|
||||||
const [explode, setExplode] = React.useState(false)
|
const [explode, setExplode] = React.useState(false)
|
||||||
@@ -87,7 +89,7 @@ test("can pass an error to useErrorHandler", async () => {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (explode) {
|
if (explode) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setError(new Error("💥 CABOOM 💥"))
|
setError(new Error('💥 CABOOM 💥'))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -96,17 +98,19 @@ test("can pass an error to useErrorHandler", async () => {
|
|||||||
render(
|
render(
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
<AsyncBomb />
|
<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 consoleError = console.error as jest.Mock<void, unknown[]>
|
||||||
const [[actualError], [componentStack]] = consoleError.mock.calls
|
const [[actualError], [componentStack]] = consoleError.mock.calls
|
||||||
const firstLineOfError = firstLine(actualError as string)
|
const firstLineOfError = firstLine(actualError as string)
|
||||||
expect(firstLineOfError).toMatchInlineSnapshot(`"Error: Uncaught [Error: 💥 CABOOM 💥]"`)
|
expect(firstLineOfError).toMatchInlineSnapshot(
|
||||||
|
`"Error: Uncaught [Error: 💥 CABOOM 💥]"`
|
||||||
|
)
|
||||||
expect(cleanStack(componentStack)).toMatchInlineSnapshot(`
|
expect(cleanStack(componentStack)).toMatchInlineSnapshot(`
|
||||||
"The above error occurred in the <AsyncBomb> component:
|
"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()
|
consoleError.mockClear()
|
||||||
|
|
||||||
// can recover
|
// 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()
|
expect(console.error).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import {render, screen} from "@testing-library/react"
|
import { render, screen } from '@testing-library/react'
|
||||||
import userEvent from "@testing-library/user-event"
|
import userEvent from '@testing-library/user-event'
|
||||||
import React from "react"
|
import React from 'react'
|
||||||
import type {FallbackProps} from "./error-boundary"
|
import type { ErrorFallbackProps } from 'next/stdlib'
|
||||||
import {ErrorBoundary, withErrorBoundary} from "./error-boundary"
|
import { ErrorBoundary, withErrorBoundary } from 'next/stdlib'
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.resetAllMocks()
|
jest.resetAllMocks()
|
||||||
@@ -10,7 +10,7 @@ afterEach(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(console, "error").mockImplementation(() => {})
|
jest.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
// afterEach(() => {
|
// afterEach(() => {
|
||||||
@@ -23,7 +23,7 @@ beforeEach(() => {
|
|||||||
// }
|
// }
|
||||||
// })
|
// })
|
||||||
|
|
||||||
function ErrorFallback({error, resetErrorBoundary}: FallbackProps) {
|
function ErrorFallback({ error, resetErrorBoundary }: ErrorFallbackProps) {
|
||||||
return (
|
return (
|
||||||
<div role="alert">
|
<div role="alert">
|
||||||
<p>Something went wrong:</p>
|
<p>Something went wrong:</p>
|
||||||
@@ -34,29 +34,29 @@ function ErrorFallback({error, resetErrorBoundary}: FallbackProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Bomb() {
|
function Bomb() {
|
||||||
throw new Error("💥 CABOOM 💥")
|
throw new Error('💥 CABOOM 💥')
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstLine = (str: string) => str.split("\n")[0]
|
const firstLine = (str: string) => str.split('\n')[0]
|
||||||
|
|
||||||
export const cleanStack = (stack: any): any => {
|
export const cleanStack = (stack: any): any => {
|
||||||
if (typeof stack === "string") {
|
if (typeof stack === 'string') {
|
||||||
return stack.replace(/\(.*\)/g, "")
|
return stack.replace(/\(.*\)/g, '')
|
||||||
}
|
}
|
||||||
if (typeof stack === "object" && stack.componentStack) {
|
if (typeof stack === 'object' && stack.componentStack) {
|
||||||
stack.componentStack = cleanStack(stack.componentStack)
|
stack.componentStack = cleanStack(stack.componentStack)
|
||||||
return stack
|
return stack
|
||||||
}
|
}
|
||||||
return stack
|
return stack
|
||||||
}
|
}
|
||||||
|
|
||||||
test("standard use-case", () => {
|
test('standard use-case', () => {
|
||||||
const consoleError = console.error as jest.Mock<void, unknown[]>
|
const consoleError = console.error as jest.Mock<void, unknown[]>
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [username, setUsername] = React.useState("")
|
const [username, setUsername] = React.useState('')
|
||||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
setUsername(e.target.value)
|
setUsername(e.target.value)
|
||||||
}
|
}
|
||||||
@@ -66,10 +66,10 @@ test("standard use-case", () => {
|
|||||||
<label htmlFor="username">Username</label>
|
<label htmlFor="username">Username</label>
|
||||||
<input type="text" id="username" onChange={handleChange} />
|
<input type="text" id="username" onChange={handleChange} />
|
||||||
</div>
|
</div>
|
||||||
<div>{username === "fail" ? "Oh no" : "things are good"}</div>
|
<div>{username === 'fail' ? 'Oh no' : 'things are good'}</div>
|
||||||
<div>
|
<div>
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
{username === "fail" ? <Bomb /> : 'type "fail"'}
|
{username === 'fail' ? <Bomb /> : 'type "fail"'}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,11 +78,11 @@ test("standard use-case", () => {
|
|||||||
|
|
||||||
render(<App />)
|
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
|
const [[actualError], [componentStack]] = consoleError.mock.calls
|
||||||
expect(firstLine(actualError as string)).toMatchInlineSnapshot(
|
expect(firstLine(actualError as string)).toMatchInlineSnapshot(
|
||||||
`"Error: Uncaught [Error: 💥 CABOOM 💥]"`,
|
`"Error: Uncaught [Error: 💥 CABOOM 💥]"`
|
||||||
)
|
)
|
||||||
expect(cleanStack(componentStack)).toMatchInlineSnapshot(`
|
expect(cleanStack(componentStack)).toMatchInlineSnapshot(`
|
||||||
"The above error occurred in the <Bomb> component:
|
"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)
|
expect(consoleError).toHaveBeenCalledTimes(2)
|
||||||
consoleError.mockClear()
|
consoleError.mockClear()
|
||||||
|
|
||||||
expect(screen.getByRole("alert")).toMatchInlineSnapshot(`
|
expect(screen.getByRole('alert')).toMatchInlineSnapshot(`
|
||||||
<div
|
<div
|
||||||
role="alert"
|
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
|
// can recover from errors when the component is rerendered and reset is clicked
|
||||||
userEvent.type(screen.getByRole("textbox", {name: /username/i}), "-not")
|
userEvent.type(screen.getByRole('textbox', { name: /username/i }), '-not')
|
||||||
userEvent.click(screen.getByRole("button", {name: /try again/i}))
|
userEvent.click(screen.getByRole('button', { name: /try again/i }))
|
||||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument()
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("fallbackRender prop", () => {
|
test('fallbackRender prop', () => {
|
||||||
const consoleError = console.error as jest.Mock<void, unknown[]>
|
const consoleError = console.error as jest.Mock<void, unknown[]>
|
||||||
|
|
||||||
const workingMessage = "Phew, we are safe!"
|
const workingMessage = 'Phew, we are safe!'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [explode, setExplode] = React.useState(true)
|
const [explode, setExplode] = React.useState(true)
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
fallbackRender={({resetErrorBoundary}) => (
|
fallbackRender={({ resetErrorBoundary }) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setExplode(false)
|
setExplode(false)
|
||||||
@@ -153,18 +153,18 @@ test("fallbackRender prop", () => {
|
|||||||
|
|
||||||
// the render prop API allows a single action to reset the app state
|
// the render prop API allows a single action to reset the app state
|
||||||
// as well as reset the ErrorBoundary 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()
|
expect(screen.getByText(workingMessage)).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("simple fallback is supported", () => {
|
test('simple fallback is supported', () => {
|
||||||
const consoleError = console.error as jest.Mock<void, unknown[]>
|
const consoleError = console.error as jest.Mock<void, unknown[]>
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<ErrorBoundary fallback={<div>Oh no</div>}>
|
<ErrorBoundary fallback={<div>Oh no</div>}>
|
||||||
<Bomb />
|
<Bomb />
|
||||||
<span>child</span>
|
<span>child</span>
|
||||||
</ErrorBoundary>,
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
expect(consoleError).toHaveBeenCalledTimes(2)
|
expect(consoleError).toHaveBeenCalledTimes(2)
|
||||||
consoleError.mockClear()
|
consoleError.mockClear()
|
||||||
@@ -172,21 +172,23 @@ test("simple fallback is supported", () => {
|
|||||||
expect(screen.queryByText(/child/i)).not.toBeInTheDocument()
|
expect(screen.queryByText(/child/i)).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("withErrorBoundary HOC", () => {
|
test('withErrorBoundary HOC', () => {
|
||||||
const consoleError = console.error as jest.Mock<void, unknown[]>
|
const consoleError = console.error as jest.Mock<void, unknown[]>
|
||||||
|
|
||||||
const onErrorHandler = jest.fn()
|
const onErrorHandler = jest.fn()
|
||||||
const Boundary = withErrorBoundary(
|
const Boundary = withErrorBoundary(
|
||||||
() => {
|
() => {
|
||||||
throw new Error("💥 CABOOM 💥")
|
throw new Error('💥 CABOOM 💥')
|
||||||
},
|
},
|
||||||
{FallbackComponent: ErrorFallback, onError: onErrorHandler},
|
{ FallbackComponent: ErrorFallback, onError: onErrorHandler }
|
||||||
)
|
)
|
||||||
render(<Boundary />)
|
render(<Boundary />)
|
||||||
|
|
||||||
const [[actualError], [componentStack]] = consoleError.mock.calls
|
const [[actualError], [componentStack]] = consoleError.mock.calls
|
||||||
const firstLineOfError = firstLine(actualError as string)
|
const firstLineOfError = firstLine(actualError as string)
|
||||||
expect(firstLineOfError).toMatchInlineSnapshot(`"Error: Uncaught [Error: 💥 CABOOM 💥]"`)
|
expect(firstLineOfError).toMatchInlineSnapshot(
|
||||||
|
`"Error: Uncaught [Error: 💥 CABOOM 💥]"`
|
||||||
|
)
|
||||||
expect(cleanStack(componentStack)).toMatchInlineSnapshot(`
|
expect(cleanStack(componentStack)).toMatchInlineSnapshot(`
|
||||||
"The above error occurred in one of your React components:
|
"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)
|
expect(consoleError).toHaveBeenCalledTimes(2)
|
||||||
consoleError.mockClear()
|
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(error.message).toMatchInlineSnapshot(`"💥 CABOOM 💥"`)
|
||||||
expect(cleanStack(onErrorComponentStack)).toMatchInlineSnapshot(`
|
expect(cleanStack(onErrorComponentStack)).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
@@ -212,10 +216,10 @@ Object {
|
|||||||
expect(onErrorHandler).toHaveBeenCalledTimes(1)
|
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 consoleError = console.error as jest.Mock<void, unknown[]>
|
||||||
|
|
||||||
const children = "Boundry children"
|
const children = 'Boundry children'
|
||||||
function App() {
|
function App() {
|
||||||
const errorBoundaryRef = React.useRef<ErrorBoundary | null>(null)
|
const errorBoundaryRef = React.useRef<ErrorBoundary | null>(null)
|
||||||
const [explode, setExplode] = React.useState(false)
|
const [explode, setExplode] = React.useState(false)
|
||||||
@@ -237,18 +241,18 @@ test("supported but undocumented reset method", () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
render(<App />)
|
render(<App />)
|
||||||
userEvent.click(screen.getByText("explode"))
|
userEvent.click(screen.getByText('explode'))
|
||||||
|
|
||||||
expect(screen.queryByText(children)).not.toBeInTheDocument()
|
expect(screen.queryByText(children)).not.toBeInTheDocument()
|
||||||
expect(consoleError).toHaveBeenCalledTimes(2)
|
expect(consoleError).toHaveBeenCalledTimes(2)
|
||||||
consoleError.mockClear()
|
consoleError.mockClear()
|
||||||
|
|
||||||
userEvent.click(screen.getByText("recover"))
|
userEvent.click(screen.getByText('recover'))
|
||||||
expect(screen.getByText(children)).toBeInTheDocument()
|
expect(screen.getByText(children)).toBeInTheDocument()
|
||||||
expect(consoleError).toHaveBeenCalledTimes(0)
|
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[]>
|
const consoleError = console.error as jest.Mock<void, unknown[]>
|
||||||
|
|
||||||
expect(() =>
|
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
|
// @ts-expect-error we're testing the runtime check of missing props here
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Bomb />
|
<Bomb />
|
||||||
</ErrorBoundary>,
|
</ErrorBoundary>
|
||||||
),
|
)
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"<ErrorBoundary> requires either a fallback, fallbackRender, or FallbackComponent prop"`,
|
`"<ErrorBoundary> requires either a fallback, fallbackRender, or FallbackComponent prop"`
|
||||||
)
|
)
|
||||||
consoleError.mockClear()
|
consoleError.mockClear()
|
||||||
})
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line max-statements
|
// 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 consoleError = console.error as jest.Mock<void, unknown[]>
|
||||||
|
|
||||||
const handleReset = jest.fn()
|
const handleReset = jest.fn()
|
||||||
const TRY_AGAIN_ARG1 = "TRY_AGAIN_ARG1"
|
const TRY_AGAIN_ARG1 = 'TRY_AGAIN_ARG1'
|
||||||
const TRY_AGAIN_ARG2 = "TRY_AGAIN_ARG2"
|
const TRY_AGAIN_ARG2 = 'TRY_AGAIN_ARG2'
|
||||||
const handleResetKeysChange = jest.fn()
|
const handleResetKeysChange = jest.fn()
|
||||||
function App() {
|
function App() {
|
||||||
const [explode, setExplode] = React.useState(false)
|
const [explode, setExplode] = React.useState(false)
|
||||||
@@ -279,12 +283,18 @@ test("supports automatic reset of error boundary when resetKeys change", () => {
|
|||||||
<div>
|
<div>
|
||||||
<button onClick={() => setExplode((e) => !e)}>toggle explode</button>
|
<button onClick={() => setExplode((e) => !e)}>toggle explode</button>
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
fallbackRender={({resetErrorBoundary}) => (
|
fallbackRender={({ resetErrorBoundary }) => (
|
||||||
<div role="alert">
|
<div role="alert">
|
||||||
<button onClick={() => resetErrorBoundary(TRY_AGAIN_ARG1, TRY_AGAIN_ARG2)}>
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
resetErrorBoundary(TRY_AGAIN_ARG1, TRY_AGAIN_ARG2)
|
||||||
|
}
|
||||||
|
>
|
||||||
Try again
|
Try again
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setExtra((e) => !e)}>toggle extra resetKey</button>
|
<button onClick={() => setExtra((e) => !e)}>
|
||||||
|
toggle extra resetKey
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
onReset={(...args) => {
|
onReset={(...args) => {
|
||||||
@@ -302,14 +312,14 @@ test("supports automatic reset of error boundary when resetKeys change", () => {
|
|||||||
render(<App />)
|
render(<App />)
|
||||||
|
|
||||||
// blow it up
|
// blow it up
|
||||||
userEvent.click(screen.getByText("toggle explode"))
|
userEvent.click(screen.getByText('toggle explode'))
|
||||||
expect(screen.getByRole("alert")).toBeInTheDocument()
|
expect(screen.getByRole('alert')).toBeInTheDocument()
|
||||||
expect(consoleError).toHaveBeenCalledTimes(2)
|
expect(consoleError).toHaveBeenCalledTimes(2)
|
||||||
consoleError.mockClear()
|
consoleError.mockClear()
|
||||||
|
|
||||||
// recover via try again button
|
// recover via try again button
|
||||||
userEvent.click(screen.getByText(/try again/i))
|
userEvent.click(screen.getByText(/try again/i))
|
||||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument()
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
||||||
expect(consoleError).not.toHaveBeenCalled()
|
expect(consoleError).not.toHaveBeenCalled()
|
||||||
expect(handleReset).toHaveBeenCalledWith(TRY_AGAIN_ARG1, TRY_AGAIN_ARG2)
|
expect(handleReset).toHaveBeenCalledWith(TRY_AGAIN_ARG1, TRY_AGAIN_ARG2)
|
||||||
expect(handleReset).toHaveBeenCalledTimes(1)
|
expect(handleReset).toHaveBeenCalledTimes(1)
|
||||||
@@ -317,59 +327,62 @@ test("supports automatic reset of error boundary when resetKeys change", () => {
|
|||||||
expect(handleResetKeysChange).not.toHaveBeenCalled()
|
expect(handleResetKeysChange).not.toHaveBeenCalled()
|
||||||
|
|
||||||
// blow it up again
|
// blow it up again
|
||||||
userEvent.click(screen.getByText("toggle explode"))
|
userEvent.click(screen.getByText('toggle explode'))
|
||||||
expect(screen.getByRole("alert")).toBeInTheDocument()
|
expect(screen.getByRole('alert')).toBeInTheDocument()
|
||||||
expect(consoleError).toHaveBeenCalledTimes(2)
|
expect(consoleError).toHaveBeenCalledTimes(2)
|
||||||
consoleError.mockClear()
|
consoleError.mockClear()
|
||||||
|
|
||||||
// recover via resetKeys change
|
// recover via resetKeys change
|
||||||
userEvent.click(screen.getByText("toggle explode"))
|
userEvent.click(screen.getByText('toggle explode'))
|
||||||
expect(handleResetKeysChange).toHaveBeenCalledWith([true], [false])
|
expect(handleResetKeysChange).toHaveBeenCalledWith([true], [false])
|
||||||
expect(handleResetKeysChange).toHaveBeenCalledTimes(1)
|
expect(handleResetKeysChange).toHaveBeenCalledTimes(1)
|
||||||
handleResetKeysChange.mockClear()
|
handleResetKeysChange.mockClear()
|
||||||
expect(handleReset).not.toHaveBeenCalled()
|
expect(handleReset).not.toHaveBeenCalled()
|
||||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument()
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
||||||
expect(consoleError).not.toHaveBeenCalled()
|
expect(consoleError).not.toHaveBeenCalled()
|
||||||
|
|
||||||
// blow it up again
|
// blow it up again
|
||||||
userEvent.click(screen.getByText("toggle explode"))
|
userEvent.click(screen.getByText('toggle explode'))
|
||||||
expect(screen.getByRole("alert")).toBeInTheDocument()
|
expect(screen.getByRole('alert')).toBeInTheDocument()
|
||||||
expect(consoleError).toHaveBeenCalledTimes(2)
|
expect(consoleError).toHaveBeenCalledTimes(2)
|
||||||
consoleError.mockClear()
|
consoleError.mockClear()
|
||||||
|
|
||||||
// toggles adding an extra resetKey to the array
|
// toggles adding an extra resetKey to the array
|
||||||
// expect error to re-render
|
// 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).toHaveBeenCalledTimes(1)
|
||||||
expect(handleResetKeysChange).toHaveBeenCalledWith([true], [true, true])
|
expect(handleResetKeysChange).toHaveBeenCalledWith([true], [true, true])
|
||||||
handleResetKeysChange.mockClear()
|
handleResetKeysChange.mockClear()
|
||||||
expect(screen.getByRole("alert")).toBeInTheDocument()
|
expect(screen.getByRole('alert')).toBeInTheDocument()
|
||||||
expect(consoleError).toHaveBeenCalledTimes(2)
|
expect(consoleError).toHaveBeenCalledTimes(2)
|
||||||
consoleError.mockClear()
|
consoleError.mockClear()
|
||||||
|
|
||||||
// toggle explode back to false
|
// toggle explode back to false
|
||||||
// expect error to re-render again
|
// expect error to re-render again
|
||||||
userEvent.click(screen.getByText("toggle explode"))
|
userEvent.click(screen.getByText('toggle explode'))
|
||||||
expect(handleReset).not.toHaveBeenCalled()
|
expect(handleReset).not.toHaveBeenCalled()
|
||||||
expect(handleResetKeysChange).toHaveBeenCalledTimes(1)
|
expect(handleResetKeysChange).toHaveBeenCalledTimes(1)
|
||||||
expect(handleResetKeysChange).toHaveBeenCalledWith([true, true], [false, true])
|
expect(handleResetKeysChange).toHaveBeenCalledWith(
|
||||||
expect(screen.getByRole("alert")).toBeInTheDocument()
|
[true, true],
|
||||||
|
[false, true]
|
||||||
|
)
|
||||||
|
expect(screen.getByRole('alert')).toBeInTheDocument()
|
||||||
handleResetKeysChange.mockClear()
|
handleResetKeysChange.mockClear()
|
||||||
expect(consoleError).toHaveBeenCalledTimes(2)
|
expect(consoleError).toHaveBeenCalledTimes(2)
|
||||||
consoleError.mockClear()
|
consoleError.mockClear()
|
||||||
|
|
||||||
// toggle extra resetKey
|
// toggle extra resetKey
|
||||||
// expect error to be reset
|
// expect error to be reset
|
||||||
userEvent.click(screen.getByText("toggle extra resetKey"))
|
userEvent.click(screen.getByText('toggle extra resetKey'))
|
||||||
expect(handleReset).not.toHaveBeenCalled()
|
expect(handleReset).not.toHaveBeenCalled()
|
||||||
expect(handleResetKeysChange).toHaveBeenCalledTimes(1)
|
expect(handleResetKeysChange).toHaveBeenCalledTimes(1)
|
||||||
expect(handleResetKeysChange).toHaveBeenCalledWith([false, true], [false])
|
expect(handleResetKeysChange).toHaveBeenCalledWith([false, true], [false])
|
||||||
handleResetKeysChange.mockClear()
|
handleResetKeysChange.mockClear()
|
||||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument()
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
||||||
expect(consoleError).not.toHaveBeenCalled()
|
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 consoleError = console.error as jest.Mock<void, unknown[]>
|
||||||
const handleResetKeysChange = jest.fn()
|
const handleResetKeysChange = jest.fn()
|
||||||
function App() {
|
function App() {
|
||||||
@@ -394,43 +407,45 @@ test("supports reset via resetKeys right after error is triggered on component m
|
|||||||
render(<App />)
|
render(<App />)
|
||||||
|
|
||||||
// it blows up on render
|
// it blows up on render
|
||||||
expect(screen.getByRole("alert")).toBeInTheDocument()
|
expect(screen.getByRole('alert')).toBeInTheDocument()
|
||||||
expect(consoleError).toHaveBeenCalledTimes(2)
|
expect(consoleError).toHaveBeenCalledTimes(2)
|
||||||
consoleError.mockClear()
|
consoleError.mockClear()
|
||||||
|
|
||||||
// recover via "toggle explode" button
|
// recover via "toggle explode" button
|
||||||
userEvent.click(screen.getByText("toggle explode"))
|
userEvent.click(screen.getByText('toggle explode'))
|
||||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument()
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
||||||
expect(consoleError).not.toHaveBeenCalled()
|
expect(consoleError).not.toHaveBeenCalled()
|
||||||
expect(handleResetKeysChange).toHaveBeenCalledWith([true], [false])
|
expect(handleResetKeysChange).toHaveBeenCalledWith([true], [false])
|
||||||
expect(handleResetKeysChange).toHaveBeenCalledTimes(1)
|
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 consoleError = console.error as jest.Mock<void, unknown[]>
|
||||||
|
|
||||||
const FancyFallback = React.forwardRef(({error}: FallbackProps) => (
|
const FancyFallback = React.forwardRef(({ error }: FallbackProps) => (
|
||||||
<div>
|
<div>
|
||||||
<p>Everything is broken. Try again</p>
|
<p>Everything is broken. Try again</p>
|
||||||
<pre>{error.message}</pre>
|
<pre>{error.message}</pre>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
FancyFallback.displayName = "FancyFallback"
|
FancyFallback.displayName = 'FancyFallback'
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
render(
|
render(
|
||||||
<ErrorBoundary FallbackComponent={FancyFallback}>
|
<ErrorBoundary FallbackComponent={FancyFallback}>
|
||||||
<Bomb />
|
<Bomb />
|
||||||
</ErrorBoundary>,
|
</ErrorBoundary>
|
||||||
),
|
)
|
||||||
).not.toThrow()
|
).not.toThrow()
|
||||||
|
|
||||||
expect(screen.getByText("Everything is broken. Try again")).toBeInTheDocument()
|
expect(
|
||||||
|
screen.getByText('Everything is broken. Try again')
|
||||||
|
).toBeInTheDocument()
|
||||||
|
|
||||||
consoleError.mockClear()
|
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[]>
|
const consoleError = console.error as jest.Mock<void, unknown[]>
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
@@ -438,8 +453,8 @@ test("should throw error if FallbackComponent is not valid", () => {
|
|||||||
// @ts-expect-error we're testing the error case
|
// @ts-expect-error we're testing the error case
|
||||||
<ErrorBoundary FallbackComponent={{}}>
|
<ErrorBoundary FallbackComponent={{}}>
|
||||||
<Bomb />
|
<Bomb />
|
||||||
</ErrorBoundary>,
|
</ErrorBoundary>
|
||||||
),
|
)
|
||||||
).toThrowError(/Element type is invalid/i)
|
).toThrowError(/Element type is invalid/i)
|
||||||
|
|
||||||
consoleError.mockClear()
|
consoleError.mockClear()
|
||||||
62
nextjs/test/unit/resolver.unit.test.ts
Normal file
62
nextjs/test/unit/resolver.unit.test.ts
Normal 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')
|
||||||
|
}
|
||||||
257
nextjs/test/unit/router-hooks.unit.test.ts
Normal file
257
nextjs/test/unit/router-hooks.unit.test.ts
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
105
nextjs/test/unit/zod-utils.unit.test.ts
Normal file
105
nextjs/test/unit/zod-utils.unit.test.ts
Normal 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' })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"reset": "rimraf node_modules && git clean -xfd packages && git clean -xfd test && git clean -xfd nextjs && yarn",
|
"reset": "rimraf node_modules && git clean -xfd packages && git clean -xfd test && git clean -xfd nextjs && yarn",
|
||||||
"publish-prep": "yarn && yarn build",
|
"publish-prep": "yarn && yarn build",
|
||||||
"prepack": "node scripts/prepack.js",
|
"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-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-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",
|
"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/ink-spinner": "3.0.0",
|
||||||
"@types/jest": "26.0.20",
|
"@types/jest": "26.0.20",
|
||||||
"@types/jsonwebtoken": "8.5.0",
|
"@types/jsonwebtoken": "8.5.0",
|
||||||
"@types/lodash": "4.14.149",
|
|
||||||
"@types/lowdb": "1.0.9",
|
"@types/lowdb": "1.0.9",
|
||||||
"@types/mem-fs": "1.1.2",
|
"@types/mem-fs": "1.1.2",
|
||||||
"@types/mem-fs-editor": "7.0.0",
|
"@types/mem-fs-editor": "7.0.0",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function AddBlitzAppRoot(): PluginObj {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapExportDefaultDeclaration(path, 'withBlitzAppRoot', '@blitzjs/core');
|
wrapExportDefaultDeclaration(path, 'withBlitzAppRoot', 'next/stdlib');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { BabelType } from 'babel-plugin-tester';
|
|||||||
* https://astexplorer.net/#/gist/dd0cdbd56a701d8c9e078d20505b3980/latest
|
* https://astexplorer.net/#/gist/dd0cdbd56a701d8c9e078d20505b3980/latest
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const defaultImportSource = '@blitzjs/core';
|
const defaultImportSource = 'next/stdlib';
|
||||||
|
|
||||||
const specialImports: Record<string, string> = {
|
const specialImports: Record<string, string> = {
|
||||||
Link: 'next/link',
|
Link: 'next/link',
|
||||||
@@ -18,12 +18,20 @@ const specialImports: Record<string, string> = {
|
|||||||
Main: 'next/document',
|
Main: 'next/document',
|
||||||
BlitzScript: 'next/document',
|
BlitzScript: 'next/document',
|
||||||
|
|
||||||
AuthenticationError: 'next/stdlib',
|
// AuthenticationError: 'next/stdlib',
|
||||||
AuthorizationError: 'next/stdlib',
|
// AuthorizationError: 'next/stdlib',
|
||||||
CSRFTokenMismatchError: 'next/stdlib',
|
// CSRFTokenMismatchError: 'next/stdlib',
|
||||||
NotFoundError: 'next/stdlib',
|
// NotFoundError: 'next/stdlib',
|
||||||
PaginationArgumentError: 'next/stdlib',
|
// PaginationArgumentError: 'next/stdlib',
|
||||||
RedirectError: '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',
|
paginate: 'next/stdlib-server',
|
||||||
isLocalhost: 'next/stdlib-server',
|
isLocalhost: 'next/stdlib-server',
|
||||||
@@ -36,6 +44,7 @@ const specialImports: Record<string, string> = {
|
|||||||
SecurePassword: 'next/stdlib-server',
|
SecurePassword: 'next/stdlib-server',
|
||||||
hash256: 'next/stdlib-server',
|
hash256: 'next/stdlib-server',
|
||||||
generateToken: 'next/stdlib-server',
|
generateToken: 'next/stdlib-server',
|
||||||
|
resolver: 'next/stdlib-server',
|
||||||
|
|
||||||
BlitzProvider: 'next/data-client',
|
BlitzProvider: 'next/data-client',
|
||||||
getAntiCSRFToken: 'next/data-client',
|
getAntiCSRFToken: 'next/data-client',
|
||||||
@@ -55,17 +64,17 @@ const specialImports: Record<string, string> = {
|
|||||||
dehydrate: 'next/data-client',
|
dehydrate: 'next/data-client',
|
||||||
invoke: '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',
|
dynamic: 'next/dynamic',
|
||||||
noSSR: '@blitzjs/core/dynamic',
|
noSSR: 'next/dynamic',
|
||||||
|
|
||||||
getConfig: '@blitzjs/core/config',
|
getConfig: 'next/config',
|
||||||
setConfig: '@blitzjs/core/config',
|
setConfig: 'next/config',
|
||||||
|
|
||||||
resolver: '@blitzjs/core/server',
|
ErrorComponent: 'next/error',
|
||||||
};
|
};
|
||||||
|
|
||||||
function RewriteImports(babel: BabelType): PluginObj {
|
function RewriteImports(babel: BabelType): PluginObj {
|
||||||
|
|||||||
@@ -54,7 +54,6 @@
|
|||||||
"@blitzjs/babel-preset": "0.41.2-canary.1",
|
"@blitzjs/babel-preset": "0.41.2-canary.1",
|
||||||
"@blitzjs/cli": "0.41.2-canary.1",
|
"@blitzjs/cli": "0.41.2-canary.1",
|
||||||
"@blitzjs/config": "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/display": "0.41.2-canary.1",
|
||||||
"@blitzjs/generator": "0.41.2-canary.1",
|
"@blitzjs/generator": "0.41.2-canary.1",
|
||||||
"@blitzjs/server": "0.41.2-canary.1",
|
"@blitzjs/server": "0.41.2-canary.1",
|
||||||
@@ -70,6 +69,7 @@
|
|||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"jest-watch-typeahead": "^0.6.1",
|
"jest-watch-typeahead": "^0.6.1",
|
||||||
"minimist": "1.2.5",
|
"minimist": "1.2.5",
|
||||||
|
"next": "0.41.2-canary.1",
|
||||||
"os-name": "^4.0.0",
|
"os-name": "^4.0.0",
|
||||||
"pkg-dir": "^5.0.0",
|
"pkg-dir": "^5.0.0",
|
||||||
"react-test-renderer": "17.0.1",
|
"react-test-renderer": "17.0.1",
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ function codegen() {
|
|||||||
// Sometimes with npm the next package is missing because of how
|
// 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
|
// we use the `npm:@blitzjs/next` syntax to install the fork at node_modules/next
|
||||||
debug("Missing next package, manually installing...")
|
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, [
|
await run("npm", ["install", "--no-save", `next@${corePkg.dependencies.next}`], root, [
|
||||||
"ignore",
|
"ignore",
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
export * from "@blitzjs/core/app"
|
export type {BlitzConfig} from "@blitzjs/config"
|
||||||
export * from "@blitzjs/core/config"
|
|
||||||
export * from "@blitzjs/core/dynamic"
|
|
||||||
export * from "@blitzjs/core/head"
|
|
||||||
export * from "@blitzjs/core"
|
|
||||||
export * from "@blitzjs/core/server"
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* IF YOU CHANGE THE BELOW EXPORTS
|
* 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 type {ImageProps, ImageLoader, ImageLoaderProps} from "next/image"
|
||||||
|
|
||||||
export * from "next/link"
|
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 {Document, DocumentHead, Html, Main, BlitzScript} from "next/document"
|
||||||
export type {DocumentProps, DocumentContext, DocumentInitialProps} from "next/document"
|
export type {DocumentProps, DocumentContext, DocumentInitialProps} from "next/document"
|
||||||
|
|
||||||
|
|||||||
@@ -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"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
8
packages/core/.gitignore
vendored
8
packages/core/.gitignore
vendored
@@ -1,8 +0,0 @@
|
|||||||
*.log
|
|
||||||
.DS_Store
|
|
||||||
node_modules
|
|
||||||
.rts2_cache_cjs
|
|
||||||
.rts2_cache_esm
|
|
||||||
.rts2_cache_umd
|
|
||||||
.rts2_cache_system
|
|
||||||
dist
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"main": "dist/blitzjs-core-app.cjs.js",
|
|
||||||
"module": "dist/blitzjs-core-app.esm.js"
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
preset: "../../jest-unit.config.js",
|
|
||||||
testEnvironment: "jest-environment-jsdom",
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
require("@testing-library/jest-dom")
|
|
||||||
process.env.BLITZ_TEST_ENVIRONMENT = true
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
@@ -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"
|
|
||||||
@@ -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"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export {default as ErrorComponent} from "next/error"
|
|
||||||
@@ -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"
|
|
||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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"})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -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"`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -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`
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
7
packages/core/types/index.d.ts
vendored
7
packages/core/types/index.d.ts
vendored
@@ -1,7 +0,0 @@
|
|||||||
// declare module '@prisma/client' {
|
|
||||||
// export class PrismaClient {
|
|
||||||
// constructor(args: any)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
export * from "../src/types"
|
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blitzjs/config": "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/display": "0.41.2-canary.1",
|
||||||
"cross-spawn": "7.0.3",
|
"cross-spawn": "7.0.3",
|
||||||
"detect-port": "1.3.0",
|
"detect-port": "1.3.0",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ nextJson.blitzVersion = nextJson.version
|
|||||||
nextJson.version = `${nextJson.nextjsVersion}-${nextJson.blitzVersion}`
|
nextJson.version = `${nextJson.nextjsVersion}-${nextJson.blitzVersion}`
|
||||||
fs.writeJSONSync(nextJsonPath, nextJson, {spaces: 2})
|
fs.writeJSONSync(nextJsonPath, nextJson, {spaces: 2})
|
||||||
|
|
||||||
const blitzCoreJsonPath = "packages/core/package.json"
|
const blitzCoreJsonPath = "packages/blitz/package.json"
|
||||||
const blitzCoreJson = fs.readJSONSync(blitzCoreJsonPath)
|
const blitzCoreJson = fs.readJSONSync(blitzCoreJsonPath)
|
||||||
blitzCoreJson.dependencies.next = `npm:@blitzjs/next@${nextJson.version}`
|
blitzCoreJson.dependencies.next = `npm:@blitzjs/next@${nextJson.version}`
|
||||||
fs.writeJSONSync(blitzCoreJsonPath, blitzCoreJson, {spaces: 2})
|
fs.writeJSONSync(blitzCoreJsonPath, blitzCoreJson, {spaces: 2})
|
||||||
|
|||||||
25
yarn.lock
25
yarn.lock
@@ -4636,16 +4636,18 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/lodash" "*"
|
"@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@*":
|
"@types/lodash@*":
|
||||||
version "4.14.166"
|
version "4.14.166"
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.166.tgz#07e7f2699a149219dbc3c35574f126ec8737688f"
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.166.tgz#07e7f2699a149219dbc3c35574f126ec8737688f"
|
||||||
integrity sha512-A3YT/c1oTlyvvW/GQqG86EyqWNrT/tisOIh2mW3YCgcx71TNjiTZA3zYZWA5BCmtsOTXjhliy4c4yEkErw6njA==
|
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":
|
"@types/long@^4.0.1":
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
|
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"
|
lodash "^4.17.20"
|
||||||
minimist "^1.2.5"
|
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:
|
htmlparser2@5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-5.0.1.tgz#7daa6fc3e35d6107ac95a4fc08781f091664f6e7"
|
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"
|
compress-commons "^3.0.0"
|
||||||
readable-stream "^3.6.0"
|
readable-stream "^3.6.0"
|
||||||
|
|
||||||
zod@3.8.1:
|
zod@3.10.1:
|
||||||
version "3.8.1"
|
version "3.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.8.1.tgz#b1173c3b4ac2a9e06d302ff580e3b41902766b9f"
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.10.1.tgz#ea5fdbb9d6ed0abc3c3000be9b768d692c2d5275"
|
||||||
integrity sha512-u4Uodl7dLh8nXZwqXL1SM5FAl5b4lXYHOxMUVb9lqhlEAZhA2znX+0oW480m0emGFMxpoRHzUncAqRkc4h8ZJA==
|
integrity sha512-ZnIzDr3vhppKW7yTAlvUQ7QJir5yoL14DgZ6DjYNb4/D4DxFdZeysF6Q5hAahU7KXtaiTgYkGO/qiAD83YN3ig==
|
||||||
|
|
||||||
zwitch@^1.0.0:
|
zwitch@^1.0.0:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
|
|||||||
Reference in New Issue
Block a user