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