Skip to content

Commit

Permalink
feat(router): Implement routing with Suspense (#8392)
Browse files Browse the repository at this point in the history
Co-authored-by: Kris Coulson <kriscoulson@gmail.com>
  • Loading branch information
2 people authored and jtoar committed Jun 8, 2023
1 parent e159375 commit 9d731dd
Show file tree
Hide file tree
Showing 24 changed files with 1,450 additions and 879 deletions.
46 changes: 22 additions & 24 deletions packages/internal/src/__tests__/nestedPages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,17 @@ describe('User specified imports, with static imports', () => {
expect(outputWithStaticImports).toContain(
`const LoginPage = {
name: "LoginPage",
loader: () => import( /* webpackChunkName: "LoginPage" */"./pages/LoginPage/LoginPage"),
prerenderLoader: name => require("./pages/LoginPage/LoginPage")
prerenderLoader: name => require("./pages/LoginPage/LoginPage"),
LazyComponent: (0, _react.lazy)(() => import( /* webpackChunkName: "LoginPage" */"./pages/LoginPage/LoginPage"))
}`
)

expect(outputWithStaticImports).toContain(
`const HomePage = {
name: "HomePage",
loader: () => import( /* webpackChunkName: "HomePage" */"./pages/HomePage/HomePage"),
prerenderLoader: name => require("./pages/HomePage/HomePage")
}`
prerenderLoader: name => require("./pages/HomePage/HomePage"),
LazyComponent: (0, _react.lazy)(() => import( /* webpackChunkName: "HomePage" */"./pages/HomePage/HomePage"))
};`
)
})
})
Expand All @@ -76,16 +76,16 @@ describe('User specified imports, with static imports', () => {
expect(outputNoStaticImports).toContain(
`const LoginPage = {
name: "LoginPage",
loader: () => import( /* webpackChunkName: "LoginPage" */"./pages/LoginPage/LoginPage"),
prerenderLoader: name => __webpack_require__(require.resolveWeak("./pages/LoginPage/LoginPage"))
prerenderLoader: name => __webpack_require__(require.resolveWeak("./pages/LoginPage/LoginPage")),
LazyComponent: (0, _react.lazy)(() => import( /* webpackChunkName: "LoginPage" */"./pages/LoginPage/LoginPage"))
}`
)

expect(outputNoStaticImports).toContain(
`const HomePage = {
name: "HomePage",
loader: () => import( /* webpackChunkName: "HomePage" */"./pages/HomePage/HomePage"),
prerenderLoader: name => __webpack_require__(require.resolveWeak("./pages/HomePage/HomePage"))
prerenderLoader: name => __webpack_require__(require.resolveWeak("./pages/HomePage/HomePage")),
LazyComponent: (0, _react.lazy)(() => import( /* webpackChunkName: "HomePage" */"./pages/HomePage/HomePage"))
}`
)
})
Expand All @@ -99,8 +99,8 @@ describe('User specified imports, with static imports', () => {
expect(outputWithStaticImports).toContain(
`const NewJobPage = {
name: "NewJobPage",
loader: () => import( /* webpackChunkName: "NewJobPage" */"./pages/Jobs/NewJobPage/NewJobPage"),
prerenderLoader: name => require("./pages/Jobs/NewJobPage/NewJobPage")
prerenderLoader: name => require("./pages/Jobs/NewJobPage/NewJobPage"),
LazyComponent: (0, _react.lazy)(() => import( /* webpackChunkName: "NewJobPage" */"./pages/Jobs/NewJobPage/NewJobPage"))
}`
)
})
Expand All @@ -110,8 +110,8 @@ describe('User specified imports, with static imports', () => {
expect(outputWithStaticImports).toContain(
`const BazingaJobProfilePageWithFunnyName = {
name: "BazingaJobProfilePageWithFunnyName",
loader: () => import( /* webpackChunkName: "BazingaJobProfilePageWithFunnyName" */"./pages/Jobs/JobProfilePage/JobProfilePage"),
prerenderLoader: name => require("./pages/Jobs/JobProfilePage/JobProfilePage")
prerenderLoader: name => require("./pages/Jobs/JobProfilePage/JobProfilePage"),
LazyComponent: (0, _react.lazy)(() => import( /* webpackChunkName: "BazingaJobProfilePageWithFunnyName" */"./pages/Jobs/JobProfilePage/JobProfilePage"))
}`
)
})
Expand Down Expand Up @@ -149,11 +149,11 @@ describe('User specified imports, with static imports', () => {
})`)
})

it("Uses the loader for a page that isn't imported", () => {
it("Uses the LazyComponent for a page that isn't imported", () => {
expect(outputNoStaticImports).toContain(`const HomePage = {
name: "HomePage",
loader: () => import( /* webpackChunkName: "HomePage" */"./pages/HomePage/HomePage"),
prerenderLoader: name => __webpack_require__(require.resolveWeak("./pages/HomePage/HomePage"))
prerenderLoader: name => __webpack_require__(require.resolveWeak("./pages/HomePage/HomePage")),
LazyComponent: (0, _react.lazy)(() => import( /* webpackChunkName: "HomePage" */"./pages/HomePage/HomePage"))
}`)
expect(outputNoStaticImports).toContain(`.createElement(_router.Route, {
path: "/",
Expand All @@ -162,14 +162,12 @@ describe('User specified imports, with static imports', () => {
})`)
})

it('Should NOT add a loader for pages that have been explicitly loaded', () => {
it('Should NOT add a LazyComponent for pages that have been explicitly loaded', () => {
expect(outputNoStaticImports).not.toContain(`const JobsJobPage = {
name: "JobsJobPage",
loader: () => import( /* webpackChunkName: "JobsJobPage" */"./pages/Jobs/JobsPage/JobsPage")`)
name: "JobsJobPage"`)

expect(outputNoStaticImports).not.toContain(`const JobsNewJobPage = {
name: "JobsNewJobPage",
loader: () => import( /* webpackChunkName: "JobsNewJobPage" */"./pages/Jobs/NewJobPage/NewJobPage")`)
name: "JobsNewJobPage"`)

expect(outputNoStaticImports).toContain(`.createElement(_router.Route, {
path: "/jobs",
Expand All @@ -187,8 +185,8 @@ describe('User specified imports, with static imports', () => {

expect(outputWithStaticImports).toContain(`const EditJobPage = {
name: "EditJobPage",
loader: () => import( /* webpackChunkName: "EditJobPage" */"./pages/Jobs/EditJobPage/EditJobPage"),
prerenderLoader: name => require("./pages/Jobs/EditJobPage/EditJobPage")
prerenderLoader: name => require("./pages/Jobs/EditJobPage/EditJobPage"),
LazyComponent: (0, _react.lazy)(() => import( /* webpackChunkName: "EditJobPage" */"./pages/Jobs/EditJobPage/EditJobPage"))
}`)

expect(outputNoStaticImports).toContain(
Expand All @@ -202,7 +200,7 @@ describe('User specified imports, with static imports', () => {

// Should not generate a loader, because page was explicitly imported
expect(outputNoStaticImports).not.toMatch(
/loader: \(\) => import\(.*"\.\/pages\/Jobs\/EditJobPage\/EditJobPage"\)/
/import\(.*"\.\/pages\/Jobs\/EditJobPage\/EditJobPage"\)/
)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@ describe('page auto loader correctly imports pages', () => {
delete process.env.RWJS_CWD
})

test('Pages get both a loader and a prerenderLoader', () => {
test('Pages get both a LazyComponent and a prerenderLoader', () => {
expect(result?.code).toContain(`const HomePage = {
name: "HomePage",
loader: () => import( /* webpackChunkName: "HomePage" */"./pages/HomePage/HomePage"),
prerenderLoader: name => __webpack_require__(require.resolveWeak("./pages/HomePage/HomePage"))`)
prerenderLoader: name => __webpack_require__(require.resolveWeak("./pages/HomePage/HomePage")),
LazyComponent: lazy(() => import( /* webpackChunkName: "HomePage" */"./pages/HomePage/HomePage"))
`)
})

test('Already imported pages are left alone.', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,33 @@ export default function (
return
}
const nodes = []

// Add "import {lazy} from 'react'"
nodes.unshift(
t.importDeclaration(
[t.importSpecifier(t.identifier('lazy'), t.identifier('lazy'))],
t.stringLiteral('react')
)
)

// Prepend all imports to the top of the file
for (const { importName, relativeImport } of pages) {
// + const <importName> = {
// const <importName> = {
// name: <importName>,
// loader: () => import(/* webpackChunkName: "<importName>" */ <relativeImportPath>)
// prerenderLoader: (name) => prerenderLoaderImpl
// LazyComponent: lazy(() => import(/* webpackChunkName: "..." */ <relativeImportPath>)
// }

/**
* Real example
* const LoginPage = {
* name: "LoginPage",
* prerenderLoader: () => __webpack_require__(require.resolveWeak("./pages/LoginPage/LoginPage")), */
// LazyComponent: lazy(() => import("/* webpackChunkName: "LoginPage" *//pages/LoginPage/LoginPage.tsx"))
/*
* }
*/

const importArgument = t.stringLiteral(relativeImport)

importArgument.leadingComments = [
Expand All @@ -140,17 +159,6 @@ export default function (
t.identifier('name'),
t.stringLiteral(importName)
),
// loader for dynamic imports (browser)
// loader: () => import(<importArgument>)
t.objectProperty(
t.identifier('loader'),
t.arrowFunctionExpression(
[],
t.callExpression(t.identifier('import'), [
importArgument,
])
)
),
// prerenderLoader for ssr/prerender and first load of
// prerendered pages in browser (csr)
// prerenderLoader: (name) => { prerenderLoaderImpl }
Expand All @@ -161,6 +169,17 @@ export default function (
prerenderLoaderImpl(prerender, vite, relativeImport, t)
)
),
t.objectProperty(
t.identifier('LazyComponent'),
t.callExpression(t.identifier('lazy'), [
t.arrowFunctionExpression(
[],
t.callExpression(t.identifier('import'), [
importArgument,
])
),
])
),
])
),
])
Expand Down
74 changes: 74 additions & 0 deletions packages/router/src/AuthenticatedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, { useCallback } from 'react'

import { Redirect } from './links'
import { routes } from './router'
import { useRouterState } from './router-context'

export function AuthenticatedRoute(props: any) {
const {
private: privateSet,
unauthenticated,
roles,
whileLoadingAuth,
children,
} = props
const routerState = useRouterState()
const {
loading: authLoading,
isAuthenticated,
hasRole,
} = routerState.useAuth()

const unauthorized = useCallback(() => {
return !(isAuthenticated && (!roles || hasRole(roles)))
}, [isAuthenticated, roles, hasRole])

// Make sure `wrappers` is always an array with at least one wrapper component
if (privateSet && unauthorized()) {
if (!unauthenticated) {
throw new Error(
'Private Sets need to specify what route to redirect unauthorized ' +
'users to by setting the `unauthenticated` prop'
)
}

if (authLoading) {
return whileLoadingAuth?.() || null
} else {
const currentLocation =
globalThis.location.pathname +
encodeURIComponent(globalThis.location.search)

if (!routes[unauthenticated]) {
throw new Error(`We could not find a route named ${unauthenticated}`)
}

let unauthenticatedPath

try {
unauthenticatedPath = routes[unauthenticated]()
} catch (e) {
if (
e instanceof Error &&
/Missing parameter .* for route/.test(e.message)
) {
throw new Error(
`Redirecting to route "${unauthenticated}" would require route ` +
'parameters, which currently is not supported. Please choose ' +
'a different route'
)
}

throw new Error(
`Could not redirect to the route named ${unauthenticated}`
)
}

return (
<Redirect to={`${unauthenticatedPath}?redirectTo=${currentLocation}`} />
)
}
}

return <>{children}</>
}
14 changes: 9 additions & 5 deletions packages/router/src/PageLoadingContext.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
import { useContext, useMemo } from 'react'
import { useContext, useState } from 'react'

import { createNamedContext } from './util'

export interface PageLoadingContextInterface {
loading: boolean
setPageLoadingContext: (loading: boolean) => void
delay?: number
}

const PageLoadingContext =
createNamedContext<PageLoadingContextInterface>('PageLoading')

interface Props {
value: PageLoadingContextInterface
children: React.ReactNode
delay?: number
}

export const PageLoadingContextProvider: React.FC<Props> = ({
value,
children,
delay = 1000,
}) => {
const memoValue = useMemo(() => ({ loading: value.loading }), [value.loading])
const [loading, setPageLoadingContext] = useState(false)

return (
<PageLoadingContext.Provider value={memoValue}>
<PageLoadingContext.Provider
value={{ loading, setPageLoadingContext, delay }}
>
{children}
</PageLoadingContext.Provider>
)
Expand Down
Loading

0 comments on commit 9d731dd

Please sign in to comment.