From 19c5c16a68db7b61d57a647cf10883029cb79ecc Mon Sep 17 00:00:00 2001
From: Tobbe Lundberg
Date: Wed, 3 Jan 2024 07:36:54 +0100
Subject: [PATCH 01/18] Router: Use a single RouterContext (#9792)
---
packages/router/src/router-context.tsx | 38 ++++++++------------------
1 file changed, 11 insertions(+), 27 deletions(-)
diff --git a/packages/router/src/router-context.tsx b/packages/router/src/router-context.tsx
index 74a421773e98..4c753e2425b8 100644
--- a/packages/router/src/router-context.tsx
+++ b/packages/router/src/router-context.tsx
@@ -1,4 +1,4 @@
-import React, { useReducer, createContext, useContext } from 'react'
+import React, { createContext, useContext, useMemo } from 'react'
import type { AuthContextInterface } from '@redwoodjs/auth'
import { useNoAuth } from '@redwoodjs/auth'
@@ -29,19 +29,6 @@ export interface RouterState {
const RouterStateContext = createContext(undefined)
-export interface RouterSetContextProps {
- setState: (newState: Partial) => void
-}
-
-const RouterSetContext = createContext<
- React.Dispatch> | undefined
->(undefined)
-
-/**
- * This file splits the context into getter and setter contexts.
- * This was originally meant to optimize the number of redraws
- * See https://kentcdodds.com/blog/how-to-optimize-your-context-value
- */
export interface RouterContextProviderProps
extends Omit {
useAuth?: UseAuth
@@ -50,10 +37,6 @@ export interface RouterContextProviderProps
children: React.ReactNode
}
-function stateReducer(state: RouterState, newState: Partial) {
- return { ...state, ...newState }
-}
-
export const RouterContextProvider: React.FC = ({
useAuth,
paramTypes,
@@ -61,18 +44,19 @@ export const RouterContextProvider: React.FC = ({
activeRouteName,
children,
}) => {
- const [state, setState] = useReducer(stateReducer, {
- useAuth: useAuth || useNoAuth,
- paramTypes,
- routes,
- activeRouteName,
- })
+ const state = useMemo(
+ () => ({
+ useAuth: useAuth || useNoAuth,
+ paramTypes,
+ routes,
+ activeRouteName,
+ }),
+ [useAuth, paramTypes, routes, activeRouteName]
+ )
return (
-
- {children}
-
+ {children}
)
}
From f0537299984c0ca4eb40a996513c39a1d85f893b Mon Sep 17 00:00:00 2001
From: Tobbe Lundberg
Date: Wed, 3 Jan 2024 08:07:14 +0100
Subject: [PATCH 02/18] useRoutePath(): Get the path for the current route by
default (#9790)
---
docs/docs/router.md | 18 ++++++++++---
.../src/__tests__/useRoutePaths.test.tsx | 25 ++++++++++++++++---
packages/router/src/useRoutePaths.ts | 12 +++++++--
3 files changed, 45 insertions(+), 10 deletions(-)
diff --git a/docs/docs/router.md b/docs/docs/router.md
index fc0ddbda938e..16783e872491 100644
--- a/docs/docs/router.md
+++ b/docs/docs/router.md
@@ -478,14 +478,24 @@ Example output:
## useRoutePath
-This is a convenience hook for when you only want the path for a single route.
+Use this hook when you only want the path for a single route. By default it
+will give you the path for the current route
```jsx
-const aboutPath = useRoutePath('about') // returns "/about"
+// returns "/about" if you're currently on https://example.org/about
+const aboutPath = useRoutePath()
```
-is the same as
+
+You can also pass in the name of a route and get the path for that route
+```jsx
+// returns "/about"
+const aboutPath = useRoutePath('about')
+```
+
+Note that the above is the same as
```jsx
const routePaths = useRoutePaths()
-const aboutPath = routePaths.about // Also returns "/about"
+// returns "/about"
+const aboutPath = routePaths.about
```
## useRouteName
diff --git a/packages/router/src/__tests__/useRoutePaths.test.tsx b/packages/router/src/__tests__/useRoutePaths.test.tsx
index a456a8bf1e70..f744213f3389 100644
--- a/packages/router/src/__tests__/useRoutePaths.test.tsx
+++ b/packages/router/src/__tests__/useRoutePaths.test.tsx
@@ -2,7 +2,9 @@
import React from 'react'
import { render } from '@testing-library/react'
+import { act } from 'react-dom/test-utils'
+import { navigate } from '../history'
import { Route, Router } from '../router'
import { Set } from '../Set'
import { useRoutePaths, useRoutePath } from '../useRoutePaths'
@@ -27,17 +29,25 @@ test('useRoutePaths and useRoutePath', async () => {
children: React.ReactNode
}
- const Layout = ({ children }: LayoutProps) => <>{children}>
+ const Layout = ({ children }: LayoutProps) => {
+ // No name means current route
+ const routePath = useRoutePath()
+
+ return (
+ <>
+
Current route path: "{routePath}"
+ {children}
+ >
+ )
+ }
const Page = () =>
Page
const TestRouter = () => (
-
+
-
-
@@ -48,4 +58,11 @@ test('useRoutePaths and useRoutePath', async () => {
await screen.findByText('Home Page')
await screen.findByText(/^My path is\s+\/$/)
await screen.findByText(/^All paths:\s+\/,\/one,\/two\/\{id:Int\}$/)
+ await screen.findByText('Current route path: "/"')
+
+ act(() => navigate('/one'))
+ await screen.findByText('Current route path: "/one"')
+
+ act(() => navigate('/two/123'))
+ await screen.findByText('Current route path: "/two/{id:Int}"')
})
diff --git a/packages/router/src/useRoutePaths.ts b/packages/router/src/useRoutePaths.ts
index e3f269e1270d..3b5a7efd2c90 100644
--- a/packages/router/src/useRoutePaths.ts
+++ b/packages/router/src/useRoutePaths.ts
@@ -1,4 +1,5 @@
import { useRouterState } from './router-context'
+import { useRouteName } from './useRouteName'
import type { GeneratedRoutesMap } from './util'
import type { AvailableRoutes } from '.'
@@ -20,8 +21,15 @@ export function useRoutePaths() {
return routePaths
}
-export function useRoutePath(routeName: keyof AvailableRoutes) {
+export function useRoutePath(routeName?: keyof AvailableRoutes) {
+ const currentRouteName = useRouteName()
const routePaths = useRoutePaths()
- return routePaths[routeName]
+ const name = routeName || currentRouteName
+
+ if (!name) {
+ return undefined
+ }
+
+ return routePaths[name]
}
From 660f03311c043f955fe58544d504ec033437e8e0 Mon Sep 17 00:00:00 2001
From: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com>
Date: Wed, 3 Jan 2024 11:34:32 +0000
Subject: [PATCH 03/18] fix(fastify): Prevent duplicate `@fastify/url-data`
registration (#9794)
---
packages/api-server/src/plugins/withFunctions.ts | 4 +++-
packages/api-server/src/plugins/withWebServer.ts | 4 +++-
packages/fastify/src/api.ts | 4 +++-
packages/fastify/src/graphql.ts | 4 +++-
packages/fastify/src/web.ts | 4 +++-
packages/web-server/src/web.ts | 4 +++-
6 files changed, 18 insertions(+), 6 deletions(-)
diff --git a/packages/api-server/src/plugins/withFunctions.ts b/packages/api-server/src/plugins/withFunctions.ts
index d985fcbb0697..dd0a2006bb79 100644
--- a/packages/api-server/src/plugins/withFunctions.ts
+++ b/packages/api-server/src/plugins/withFunctions.ts
@@ -13,7 +13,9 @@ const withFunctions = async (
) => {
const { apiRootPath } = options
// Add extra fastify plugins
- fastify.register(fastifyUrlData)
+ if (!fastify.hasPlugin('@fastify/url-data')) {
+ await fastify.register(fastifyUrlData)
+ }
// Fastify v4 must await the fastifyRawBody plugin
// registration to ensure the plugin is ready
diff --git a/packages/api-server/src/plugins/withWebServer.ts b/packages/api-server/src/plugins/withWebServer.ts
index 98c5b9d8ce6c..d265c428159f 100644
--- a/packages/api-server/src/plugins/withWebServer.ts
+++ b/packages/api-server/src/plugins/withWebServer.ts
@@ -28,7 +28,9 @@ const withWebServer = async (
fastify: FastifyInstance,
options: WebServerArgs
) => {
- fastify.register(fastifyUrlData)
+ if (!fastify.hasPlugin('@fastify/url-data')) {
+ await fastify.register(fastifyUrlData)
+ }
const prerenderedFiles = findPrerenderedHtml()
const indexPath = getFallbackIndexPath()
diff --git a/packages/fastify/src/api.ts b/packages/fastify/src/api.ts
index 47e5205d01e0..d060edbd3656 100644
--- a/packages/fastify/src/api.ts
+++ b/packages/fastify/src/api.ts
@@ -14,7 +14,9 @@ export async function redwoodFastifyAPI(
opts: RedwoodFastifyAPIOptions,
done: HookHandlerDoneFunction
) {
- fastify.register(fastifyUrlData)
+ if (!fastify.hasPlugin('@fastify/url-data')) {
+ await fastify.register(fastifyUrlData)
+ }
await fastify.register(fastifyRawBody)
// TODO: This should be refactored to only be defined once and it might not live here
diff --git a/packages/fastify/src/graphql.ts b/packages/fastify/src/graphql.ts
index bf564f33aace..b3d1ef5d06b1 100644
--- a/packages/fastify/src/graphql.ts
+++ b/packages/fastify/src/graphql.ts
@@ -34,7 +34,9 @@ export async function redwoodFastifyGraphQLServer(
// These two plugins are needed to transform a Fastify Request to a Lambda event
// which is used by the RedwoodGraphQLContext and mimics the behavior of the
// api-server withFunction plugin
- fastify.register(fastifyUrlData)
+ if (!fastify.hasPlugin('@fastify/url-data')) {
+ await fastify.register(fastifyUrlData)
+ }
await fastify.register(fastifyRawBody)
try {
diff --git a/packages/fastify/src/web.ts b/packages/fastify/src/web.ts
index 93bd0cc89e7e..b8d1bbac26e7 100644
--- a/packages/fastify/src/web.ts
+++ b/packages/fastify/src/web.ts
@@ -21,7 +21,9 @@ export async function redwoodFastifyWeb(
opts: RedwoodFastifyWebOptions,
done: HookHandlerDoneFunction
) {
- fastify.register(fastifyUrlData)
+ if (!fastify.hasPlugin('@fastify/url-data')) {
+ await fastify.register(fastifyUrlData)
+ }
const prerenderedFiles = findPrerenderedHtml()
// Serve prerendered HTML directly, instead of the index.
diff --git a/packages/web-server/src/web.ts b/packages/web-server/src/web.ts
index 8b115880335a..1a60ee88efb2 100644
--- a/packages/web-server/src/web.ts
+++ b/packages/web-server/src/web.ts
@@ -27,7 +27,9 @@ export async function redwoodFastifyWeb(
opts: RedwoodFastifyWebOptions,
done: HookHandlerDoneFunction
) {
- fastify.register(fastifyUrlData)
+ if (!fastify.hasPlugin('@fastify/url-data')) {
+ await fastify.register(fastifyUrlData)
+ }
const prerenderedFiles = findPrerenderedHtml()
// Serve prerendered HTML directly, instead of the index.
From c77ebee0fec7fc98dd6ca21210816512912e3f63 Mon Sep 17 00:00:00 2001
From: Tobbe Lundberg
Date: Wed, 3 Jan 2024 13:04:29 +0100
Subject: [PATCH 04/18] Add routeParams to useMatch (#9793)
Make it possible to specify route param values that need to match.
If this is your route: ``
And you want to only match posts from 2001 you can now do this:
`useMatch('/blog/{year}/{month}/{day}', { routeParams: { year: '2001' }
})`
This is **finally** a solution to matching route paths. The work started
in #7469, but we were never able to come up with an api/dx that we
really liked. This PR and #9755 together however provides a solution
that we're much more happy with, and that also supports the use case
outlined in that original PR.
Here's the example from #7469 as it could be solved with the code in
this PR
```jsx
const Navbar () => {
const { project } = useParams()
const routePaths = useRoutePaths()
const modes = [
{
name: "Info",
route: routes.info({ project }),
match: useMatch(routePaths.info), // using the hook together with routePaths
},
{
name: "Compare",
route: routes.compare({ project, id: "1" }),
match: useMatch(useRoutePath('compare')), // alternative to the above
},
// ...
]
return (
<>
{modes.map((x) => )}
>
)
}
```
And, as described at the top of this description, we can also be more
specific than in that example if needed. Like if we only wanted to match
a specific project on the "Compare" route we could do this:
```jsx
const modes = [
{
name: "Info",
route: routes.info({ project }),
match: useMatch(routePaths.info),
},
{
name: "Compare against Alpha",
route: routes.compare({ project, id: "1" }),
match: useMatch(useRoutePath('compare'), { routeParams: { project: 'alpha' } }),
},
{
name: "Compare against Beta",
route: routes.compare({ project, id: "1" }),
match: useMatch(useRoutePath('compare'), { routeParams: { project: 'beta' } }),
},
// ...
]
```
Here's another example
```jsx
const exampleRoutePath = useRoutePath('example')
// => '/{dynamic}/{path}'
const matchOnlyDog = useMatch(exampleRoutePath, { routeParams: { dynamic: 'dog' }})
const matchFullyDynamic = useMatch(exampleRoutePath)
```
In the above example, if the current page url was
`https://example.org/dog/fido` then both `matchOnlyDog` and
`matchFullyDynamic` would have `match: true`.
If the current page instead was `https://example.org/cat/garfield` then
only `matchFullyDynamic` would match
(This PR replaces #9774)
---
docs/docs/router.md | 36 +++
.../router/src/__tests__/useMatch.test.tsx | 224 +++++++++++++++++-
packages/router/src/useMatch.ts | 38 ++-
3 files changed, 294 insertions(+), 4 deletions(-)
diff --git a/docs/docs/router.md b/docs/docs/router.md
index 16783e872491..f07df829f4ff 100644
--- a/docs/docs/router.md
+++ b/docs/docs/router.md
@@ -278,6 +278,42 @@ const CustomLink = ({ to, ...rest }) => {
}
```
+Passing in `routeParams` you can make it match only on specific route parameter
+values.
+
+```jsx
+const match = useMatch('/product/{category}/{id}', {
+ routeParams: { category: 'shirts' }
+})
+```
+
+The above example will match /product/shirts/213, but not /product/pants/213
+(whereas not specifying `routeParams` at all would match both).
+
+To get the path you need to pass to `useMatch` you can use
+[`useRoutePaths`](#useroutepaths) or [`useRoutePath`](#useroutepath)
+
+Here's an example:
+
+```jsx
+
+
+const animalRoutePath = useRoutePath('animal')
+// => '/{animal}/{name}'
+
+const matchOnlyDog = useMatch(animalRoutePath, { routeParams: { animal: 'dog' }})
+const matchFullyDynamic = useMatch(animalRoutePath)
+```
+
+In the above example, if the current page url was
+`https://example.org/dog/fido` then both `matchOnlyDog` and `matchFullyDynamic`
+would have `match: true`.
+
+If the current page instead was `https://example.org/cat/garfield` then only
+`matchFullyDynamic` would match
+
+See below for more info on route parameters.
+
## Route parameters
To match variable data in a path, you can use route parameters, which are specified by a parameter name surrounded by curly braces:
diff --git a/packages/router/src/__tests__/useMatch.test.tsx b/packages/router/src/__tests__/useMatch.test.tsx
index 89d7bcdd8419..ae53a231b06b 100644
--- a/packages/router/src/__tests__/useMatch.test.tsx
+++ b/packages/router/src/__tests__/useMatch.test.tsx
@@ -1,6 +1,8 @@
import React from 'react'
-import { render } from '@testing-library/react'
+import '@testing-library/jest-dom'
+
+import { render, renderHook as tlrRenderHook } from '@testing-library/react'
import { Link } from '../links'
import { LocationProvider } from '../location'
@@ -97,4 +99,224 @@ describe('useMatch', () => {
expect(getByText(/Dunder Mifflin/)).toHaveStyle('color: red')
})
+
+ describe('routeParams', () => {
+ const mockLocation = createDummyLocation('/dummy-location')
+
+ type CallbackType = () => ReturnType
+ function renderHook(cb: CallbackType) {
+ return tlrRenderHook(cb, {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ })
+ }
+
+ function setLocation(pathname: string, search = '') {
+ mockLocation.pathname = pathname
+ mockLocation.search = search
+ }
+
+ afterEach(() => {
+ setLocation('/dummy-location')
+ })
+
+ it('matches a path with literal route param', () => {
+ setLocation('/test-path/foobar')
+
+ const { result } = renderHook(() => useMatch('/test-path/{param}'))
+
+ expect(result.current.match).toBeTruthy()
+ })
+
+ it('matches a path with given route param value', () => {
+ setLocation('/posts/uuid-string')
+
+ const { result } = renderHook(() =>
+ useMatch('/posts/{id}', { routeParams: { id: 'uuid-string' } })
+ )
+
+ expect(result.current.match).toBeTruthy()
+ })
+
+ it("doesn't match a path with different route param value", () => {
+ setLocation('/posts/uuid-string')
+
+ const { result } = renderHook(() =>
+ useMatch('/posts/{id}', { routeParams: { id: 'other-uuid-string' } })
+ )
+
+ expect(result.current.match).toBeFalsy()
+ })
+
+ it('matches a path with default param type', () => {
+ setLocation('/posts/123')
+
+ const { result } = renderHook(() =>
+ useMatch('/posts/{id}', { routeParams: { id: '123' } })
+ )
+
+ expect(result.current.match).toBeTruthy()
+ })
+
+ it('matches a path with a specified param type', () => {
+ setLocation('/posts/123')
+
+ const { result } = renderHook(() =>
+ useMatch('/posts/{id:Int}', { routeParams: { id: 123 } })
+ )
+
+ expect(result.current.match).toBeTruthy()
+ })
+
+ it("doesn't match a path with a specified param type with different value", () => {
+ setLocation('/posts/123')
+
+ const { result } = renderHook(() =>
+ useMatch('/posts/{id:Int}', { routeParams: { id: '123' } })
+ )
+
+ expect(result.current.match).toBeFalsy()
+ })
+
+ it('matches with a subset of param values specified (year, month)', () => {
+ setLocation('/year/1970/month/08/day/21')
+
+ const { result } = renderHook(() =>
+ useMatch('/year/{year}/month/{month}/day/{day}', {
+ routeParams: { year: '1970', month: '08' },
+ })
+ )
+
+ expect(result.current.match).toBeTruthy()
+ })
+
+ it('matches with a subset of param values specified (month)', () => {
+ setLocation('/year/1970/month/08/day/21')
+
+ const { result } = renderHook(() =>
+ useMatch('/year/{year}/month/{month}/day/{day}', {
+ routeParams: { month: '08' },
+ })
+ )
+
+ expect(result.current.match).toBeTruthy()
+ })
+
+ it('matches with a subset of param values specified (day)', () => {
+ const useMatchHook = () =>
+ useMatch('/year/{year}/month/{month}/day/{day}', {
+ routeParams: { day: '21' },
+ })
+
+ setLocation('/year/1970/month/08/day/21')
+ const { result: result1970 } = renderHook(useMatchHook)
+ expect(result1970.current.match).toBeTruthy()
+
+ setLocation('/year/1970/month/01/day/21')
+ const { result: resultJan } = renderHook(useMatchHook)
+ expect(resultJan.current.match).toBeTruthy()
+
+ setLocation('/year/2024/month/08/day/21')
+ const { result: result2024 } = renderHook(useMatchHook)
+ expect(result2024.current.match).toBeTruthy()
+ })
+
+ it("doesn't match with a subset of wrong param values specified (month)", () => {
+ setLocation('/year/1970/month/08/day/21')
+
+ const { result } = renderHook(() =>
+ useMatch('/year/{year}/month/{month}/day/{day}', {
+ routeParams: { month: '01' },
+ })
+ )
+
+ expect(result.current.match).toBeFalsy()
+ })
+
+ it("doesn't match with a subset of wrong param values specified (day)", () => {
+ setLocation('/year/1970/month/08/day/21')
+
+ const { result } = renderHook(() =>
+ useMatch('/year/{year}/month/{month}/day/{day}', {
+ routeParams: { day: '31' },
+ })
+ )
+
+ expect(result.current.match).toBeFalsy()
+ })
+ })
+
+ describe('routeParams + searchParams', () => {
+ const mockLocation = createDummyLocation('/dummy-location')
+
+ type CallbackType = () => ReturnType
+ function renderHook(cb: CallbackType) {
+ return tlrRenderHook(cb, {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ })
+ }
+
+ function setLocation(pathname: string, search = '') {
+ mockLocation.pathname = pathname
+ mockLocation.search = search
+ }
+
+ afterEach(() => {
+ setLocation('/dummy-location')
+ })
+
+ it('matches a path with literal route param', () => {
+ setLocation('/test-path/foobar', '?s1=one&s2=two')
+
+ const { result } = renderHook(() => useMatch('/test-path/{param}'))
+
+ expect(result.current.match).toBeTruthy()
+ })
+
+ it('matches a path with literal route param and given searchParam', () => {
+ setLocation('/test-path/foobar', '?s1=one&s2=two')
+
+ const { result } = renderHook(() =>
+ useMatch('/test-path/{param}', {
+ searchParams: [{ s1: 'one' }],
+ })
+ )
+
+ expect(result.current.match).toBeTruthy()
+ })
+
+ it("doesn't match a path with wrong route param value and given searchParam", () => {
+ setLocation('/test-path/foobar', '?s1=one&s2=two')
+
+ const { result } = renderHook(() =>
+ useMatch('/test-path/{param}', {
+ routeParams: { param: 'wrong' },
+ searchParams: [{ s1: 'one' }],
+ })
+ )
+
+ expect(result.current.match).toBeFalsy()
+ })
+
+ it('matches a deeper path with matchSubPaths', () => {
+ setLocation('/test-path/foobar/fizz/buzz', '?s1=one&s2=two')
+
+ const { result } = renderHook(() =>
+ useMatch('/test-path/{param}/{param-two}', {
+ routeParams: { ['param-two']: 'fizz' },
+ searchParams: [{ s1: 'one' }],
+ matchSubPaths: true,
+ })
+ )
+
+ expect(result.current.match).toBeTruthy()
+ })
+ })
})
diff --git a/packages/router/src/useMatch.ts b/packages/router/src/useMatch.ts
index 180e7a93e094..c2c5ac52e943 100644
--- a/packages/router/src/useMatch.ts
+++ b/packages/router/src/useMatch.ts
@@ -3,15 +3,17 @@ import { matchPath } from './util'
import type { FlattenSearchParams } from './util'
type UseMatchOptions = {
+ routeParams?: Record
searchParams?: FlattenSearchParams
matchSubPaths?: boolean
}
/**
* Returns an object of { match: boolean; params: Record; }
- * if the path matches the current location match will be true.
+ * If the path matches the current location `match` will be true.
* Params will be an object of the matched params, if there are any.
*
+ * Provide routeParams options to match specific route param values
* Provide searchParams options to match the current location.search
*
* This is useful for components that need to know "active" state, e.g.
@@ -30,8 +32,11 @@ type UseMatchOptions = {
*
* Match sub paths
* const match = useMatch('/product', { matchSubPaths: true })
+ *
+ * Match only specific route param values
+ * const match = useMatch('/product/{category}/{id}', { routeParams: { category: 'shirts' } })
*/
-export const useMatch = (pathname: string, options?: UseMatchOptions) => {
+export const useMatch = (routePath: string, options?: UseMatchOptions) => {
const location = useLocation()
if (!location) {
return { match: false }
@@ -54,7 +59,34 @@ export const useMatch = (pathname: string, options?: UseMatchOptions) => {
}
}
- return matchPath(pathname, location.pathname, {
+ const matchInfo = matchPath(routePath, location.pathname, {
matchSubPaths: options?.matchSubPaths,
})
+
+ if (!matchInfo.match) {
+ return { match: false }
+ }
+
+ const routeParams = Object.entries(options?.routeParams || {})
+
+ if (routeParams.length > 0) {
+ if (!isMatchWithParams(matchInfo) || !matchInfo.params) {
+ return { match: false }
+ }
+
+ // If paramValues were given, they must all match
+ const isParamMatch = routeParams.every(([key, value]) => {
+ return matchInfo.params[key] === value
+ })
+
+ if (!isParamMatch) {
+ return { match: false }
+ }
+ }
+
+ return matchInfo
+}
+
+function isMatchWithParams(match: unknown): match is { params: any } {
+ return match !== null && typeof match === 'object' && 'params' in match
}
From 404c22f159cfd7268cf542bf73915574586d7767 Mon Sep 17 00:00:00 2001
From: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com>
Date: Wed, 3 Jan 2024 12:30:13 +0000
Subject: [PATCH 05/18] chore(data-migration): Fix test exit code (#9795)
Co-authored-by: Tobbe Lundberg
---
.../cli-packages/dataMigrate/src/__tests__/upHandler.test.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/packages/cli-packages/dataMigrate/src/__tests__/upHandler.test.ts b/packages/cli-packages/dataMigrate/src/__tests__/upHandler.test.ts
index 2fbf0f457bbc..8942a94973cf 100644
--- a/packages/cli-packages/dataMigrate/src/__tests__/upHandler.test.ts
+++ b/packages/cli-packages/dataMigrate/src/__tests__/upHandler.test.ts
@@ -249,6 +249,10 @@ describe('upHandler', () => {
distPath: getPaths().api.dist,
})
+ // The handler will error and set the exit code to 1, we must revert that
+ // or test suite itself will fail.
+ process.exitCode = 0
+
expect(console.info.mock.calls[0][0]).toMatch(
'1 data migration(s) completed successfully.'
)
From 6e1d0c47d8f267eb7003b04ad3cf94c5d6a74eb1 Mon Sep 17 00:00:00 2001
From: Tobbe Lundberg
Date: Wed, 3 Jan 2024 15:05:54 +0100
Subject: [PATCH 06/18] data migrate: Clean up upHandler test (#9796)
---
.../src/__tests__/upHandler.test.ts | 41 +++++++++++++------
1 file changed, 28 insertions(+), 13 deletions(-)
diff --git a/packages/cli-packages/dataMigrate/src/__tests__/upHandler.test.ts b/packages/cli-packages/dataMigrate/src/__tests__/upHandler.test.ts
index 8942a94973cf..4199e214e443 100644
--- a/packages/cli-packages/dataMigrate/src/__tests__/upHandler.test.ts
+++ b/packages/cli-packages/dataMigrate/src/__tests__/upHandler.test.ts
@@ -8,6 +8,25 @@ import { handler, NO_PENDING_MIGRATIONS_MESSAGE } from '../commands/upHandler'
const redwoodProjectPath = '/redwood-app'
+let consoleLogMock: jest.SpyInstance
+let consoleInfoMock: jest.SpyInstance
+let consoleErrorMock: jest.SpyInstance
+let consoleWarnMock: jest.SpyInstance
+
+beforeEach(() => {
+ consoleLogMock = jest.spyOn(console, 'log').mockImplementation()
+ consoleInfoMock = jest.spyOn(console, 'info').mockImplementation()
+ consoleErrorMock = jest.spyOn(console, 'error').mockImplementation()
+ consoleWarnMock = jest.spyOn(console, 'warn').mockImplementation()
+})
+
+afterEach(() => {
+ consoleLogMock.mockRestore()
+ consoleInfoMock.mockRestore()
+ consoleErrorMock.mockRestore()
+ consoleWarnMock.mockRestore()
+})
+
jest.mock('fs', () => require('memfs').fs)
const mockDataMigrations: { current: any[] } = { current: [] }
@@ -146,8 +165,6 @@ const ranDataMigration = {
describe('upHandler', () => {
it("noops if there's no data migrations directory", async () => {
- console.info = jest.fn()
-
vol.fromNestedJSON(
{
'redwood.toml': '',
@@ -174,7 +191,9 @@ describe('upHandler', () => {
distPath: getPaths().api.dist,
})
- expect(console.info.mock.calls[0][0]).toMatch(NO_PENDING_MIGRATIONS_MESSAGE)
+ expect(consoleInfoMock.mock.calls[0][0]).toMatch(
+ NO_PENDING_MIGRATIONS_MESSAGE
+ )
})
it("noops if there's no pending migrations", async () => {
@@ -199,21 +218,17 @@ describe('upHandler', () => {
redwoodProjectPath
)
- console.info = jest.fn()
-
await handler({
importDbClientFromDist: true,
distPath: getPaths().api.dist,
})
- expect(console.info.mock.calls[0][0]).toMatch(NO_PENDING_MIGRATIONS_MESSAGE)
+ expect(consoleInfoMock.mock.calls[0][0]).toMatch(
+ NO_PENDING_MIGRATIONS_MESSAGE
+ )
})
it('runs pending migrations', async () => {
- console.info = jest.fn()
- console.error = jest.fn()
- console.warn = jest.fn()
-
mockDataMigrations.current = [
{
version: '20230822075441',
@@ -253,13 +268,13 @@ describe('upHandler', () => {
// or test suite itself will fail.
process.exitCode = 0
- expect(console.info.mock.calls[0][0]).toMatch(
+ expect(consoleInfoMock.mock.calls[0][0]).toMatch(
'1 data migration(s) completed successfully.'
)
- expect(console.error.mock.calls[1][0]).toMatch(
+ expect(consoleErrorMock.mock.calls[1][0]).toMatch(
'1 data migration(s) exited with errors.'
)
- expect(console.warn.mock.calls[0][0]).toMatch(
+ expect(consoleWarnMock.mock.calls[0][0]).toMatch(
'1 data migration(s) skipped due to previous error'
)
})
From fe92493ba398c6620c9354038f8215bab4cce132 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 3 Jan 2024 15:24:58 +0100
Subject: [PATCH 07/18] fix(deps): update dependency sqlite to v5 (#9698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dominic Saadi
Co-authored-by: Tobbe Lundberg
---
packages/studio/package.json | 2 +-
yarn.lock | 10 +++++-----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/packages/studio/package.json b/packages/studio/package.json
index 5969ec1d396b..aec9f3bff4ff 100644
--- a/packages/studio/package.json
+++ b/packages/studio/package.json
@@ -47,7 +47,7 @@
"qs": "6.11.2",
"smtp-server": "3.13.0",
"split2": "4.2.0",
- "sqlite": "4.2.1",
+ "sqlite": "5.1.1",
"sqlite3": "5.1.6",
"uuid": "9.0.1",
"yargs": "17.7.2"
diff --git a/yarn.lock b/yarn.lock
index f9958be5f81f..04c898328bb6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9447,7 +9447,7 @@ __metadata:
react-toastify: "npm:9.1.3"
smtp-server: "npm:3.13.0"
split2: "npm:4.2.0"
- sqlite: "npm:4.2.1"
+ sqlite: "npm:5.1.1"
sqlite3: "npm:5.1.6"
tailwindcss: "npm:3.3.5"
typescript: "npm:5.3.3"
@@ -32924,10 +32924,10 @@ __metadata:
languageName: node
linkType: hard
-"sqlite@npm:4.2.1":
- version: 4.2.1
- resolution: "sqlite@npm:4.2.1"
- checksum: 55cd4161aedcf41e3ca05990fd036d38a248503f0d2c7c5df67ae01c17059188c38219fbc679965dcab26c5d5a9431b25f12f9c46d65f377d8d851c5a4d48334
+"sqlite@npm:5.1.1":
+ version: 5.1.1
+ resolution: "sqlite@npm:5.1.1"
+ checksum: d656055cd8d4c4637c8ad4d14d4c8e04a5fd44ede5eae54f36deefa844724588e6c45dbb50ab45954fca24ba1362b6936df1bd42ad0c511275e6dbcffbb62e52
languageName: node
linkType: hard
From c80c5128392a95816a8367e843ef07be6a16eeac Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 3 Jan 2024 15:01:42 +0000
Subject: [PATCH 08/18] fix(deps): update dependency react-helmet-async to v2
(#9697)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dominic Saadi
Co-authored-by: Tobbe Lundberg
---
packages/web/package.json | 2 +-
yarn.lock | 16 +++++++---------
2 files changed, 8 insertions(+), 10 deletions(-)
diff --git a/packages/web/package.json b/packages/web/package.json
index a460790bbe9b..62443d9ac87a 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -44,7 +44,7 @@
"graphql": "16.8.1",
"graphql-sse": "2.4.0",
"graphql-tag": "2.12.6",
- "react-helmet-async": "1.3.0",
+ "react-helmet-async": "2.0.3",
"react-hot-toast": "2.4.1",
"stacktracey": "2.1.8",
"ts-toolbelt": "9.6.0"
diff --git a/yarn.lock b/yarn.lock
index 04c898328bb6..3c1f1c57e253 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9638,7 +9638,7 @@ __metadata:
nodemon: "npm:3.0.2"
react: "npm:0.0.0-experimental-e5205658f-20230913"
react-dom: "npm:0.0.0-experimental-e5205658f-20230913"
- react-helmet-async: "npm:1.3.0"
+ react-helmet-async: "npm:2.0.3"
react-hot-toast: "npm:2.4.1"
stacktracey: "npm:2.1.8"
ts-toolbelt: "npm:9.6.0"
@@ -30507,7 +30507,7 @@ __metadata:
languageName: node
linkType: hard
-"react-fast-compare@npm:^3.2.0":
+"react-fast-compare@npm:^3.2.2":
version: 3.2.2
resolution: "react-fast-compare@npm:3.2.2"
checksum: 0bbd2f3eb41ab2ff7380daaa55105db698d965c396df73e6874831dbafec8c4b5b08ba36ff09df01526caa3c61595247e3269558c284e37646241cba2b90a367
@@ -30530,19 +30530,17 @@ __metadata:
languageName: node
linkType: hard
-"react-helmet-async@npm:1.3.0":
- version: 1.3.0
- resolution: "react-helmet-async@npm:1.3.0"
+"react-helmet-async@npm:2.0.3":
+ version: 2.0.3
+ resolution: "react-helmet-async@npm:2.0.3"
dependencies:
- "@babel/runtime": "npm:^7.12.5"
invariant: "npm:^2.2.4"
- prop-types: "npm:^15.7.2"
- react-fast-compare: "npm:^3.2.0"
+ react-fast-compare: "npm:^3.2.2"
shallowequal: "npm:^1.1.0"
peerDependencies:
react: ^16.6.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0
- checksum: 8f3e6d26beff61d2ed18f7b41561df3e4d83a7582914c7196aa65158c7f3cce939276547d7a0b8987952d9d44131406df74efba02d1f8fa8a3940b49e6ced70b
+ checksum: 3ac58fac566e3f4951c3853975afd57dbc3af1442a897391a226fb1a54cc757506912d13485e897c61ecefe5d97e673ee91688b11ff20eb769b172c0309514ee
languageName: node
linkType: hard
From 9d9d373562ac9c09ff5c202fcfb88f9dfd95b995 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 3 Jan 2024 15:06:00 +0000
Subject: [PATCH 09/18] chore(deps): update dependency
@apollo/experimental-nextjs-app-support to v0.5.2 (#9716)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)
This PR contains the following updates:
| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[@apollo/experimental-nextjs-app-support](https://togithub.com/apollographql/apollo-client-nextjs)
| [`0.5.1` ->
`0.5.2`](https://renovatebot.com/diffs/npm/@apollo%2fexperimental-nextjs-app-support/0.5.1/0.5.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@apollo%2fexperimental-nextjs-app-support/0.5.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@apollo%2fexperimental-nextjs-app-support/0.5.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@apollo%2fexperimental-nextjs-app-support/0.5.1/0.5.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@apollo%2fexperimental-nextjs-app-support/0.5.1/0.5.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
---
### Release Notes
apollographql/apollo-client-nextjs
(@apollo/experimental-nextjs-app-support)
###
[`v0.5.2`](https://togithub.com/apollographql/apollo-client-nextjs/releases/tag/v0.5.2):
0.5.2: Prepare for changes in upcoming Apollo Client 3.9 pre-release
versions
[Compare
Source](https://togithub.com/apollographql/apollo-client-nextjs/compare/419072f9da36d9dbebc2ad6d367b8510d0fed994...v0.5.2)
Upcoming pre-release versions of Apollo Client 3.9 contain a change to
an internal data structure that is used by this package. (See
[https://github.com/apollographql/apollo-client/pull/11345](https://togithub.com/apollographql/apollo-client/pull/11345)
)
This release prepares for that change and ensures backwards
compatibility with Apollo Client 3.8 as well as the upcoming 3.9
versions.
#### What's Changed
- prepare for change in `QueryManager.inFlightLinkObservable` (Apollo
Client 3.9 beta) by [@phryneas](https://togithub.com/phryneas) in
[https://github.com/apollographql/apollo-client-nextjs/pull/144](https://togithub.com/apollographql/apollo-client-nextjs/pull/144)
**Full Changelog**:
https://github.com/apollographql/apollo-client-nextjs/compare/v.0.5.1...v0.5.2
---
### Configuration
π **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).
π¦ **Automerge**: Enabled.
β» **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.
π **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] If you want to rebase/retry this PR, check
this box
---
This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/redwoodjs/redwood).
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tobbe Lundberg
---
packages/web/package.json | 2 +-
yarn.lock | 10 +++++-----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/packages/web/package.json b/packages/web/package.json
index 62443d9ac87a..b1866bb1b94a 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -50,7 +50,7 @@
"ts-toolbelt": "9.6.0"
},
"devDependencies": {
- "@apollo/experimental-nextjs-app-support": "0.5.1",
+ "@apollo/experimental-nextjs-app-support": "0.5.2",
"@babel/cli": "7.23.4",
"@babel/core": "^7.22.20",
"@testing-library/jest-dom": "6.1.5",
diff --git a/yarn.lock b/yarn.lock
index 3c1f1c57e253..56ec480e1eb5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -145,9 +145,9 @@ __metadata:
languageName: node
linkType: hard
-"@apollo/experimental-nextjs-app-support@npm:0.5.1":
- version: 0.5.1
- resolution: "@apollo/experimental-nextjs-app-support@npm:0.5.1"
+"@apollo/experimental-nextjs-app-support@npm:0.5.2":
+ version: 0.5.2
+ resolution: "@apollo/experimental-nextjs-app-support@npm:0.5.2"
dependencies:
server-only: "npm:^0.0.1"
superjson: "npm:^1.12.2"
@@ -156,7 +156,7 @@ __metadata:
"@apollo/client": ">=3.8.0-rc || ^3.8.0 || >=3.9.0-alpha || >=3.9.0-beta || >=3.9.0-rc"
next: ^13.4.1 || ^14.0.0
react: ^18
- checksum: fe6df5624df7e6268eb3dde7c34c78350ed37efcfa5c317c6f6d9705ab62a513d1950e7bd17748853103d96208e57934d1841b52454165b723237585998bdcae
+ checksum: 3070a5a02dcb4b62cf813379335d1c639371ba36f8d5553af6e45201cdf6efb8fab3fcf68e74567e5110dd5b76047f5aa310b6bfef8f36a660be88de16906c95
languageName: node
linkType: hard
@@ -9621,7 +9621,7 @@ __metadata:
resolution: "@redwoodjs/web@workspace:packages/web"
dependencies:
"@apollo/client": "npm:3.8.8"
- "@apollo/experimental-nextjs-app-support": "npm:0.5.1"
+ "@apollo/experimental-nextjs-app-support": "npm:0.5.2"
"@babel/cli": "npm:7.23.4"
"@babel/core": "npm:^7.22.20"
"@babel/runtime-corejs3": "npm:7.23.6"
From 5734dcc545bb1521c02b8d7ac398511923e76170 Mon Sep 17 00:00:00 2001
From: Tobbe Lundberg
Date: Wed, 3 Jan 2024 19:19:17 +0100
Subject: [PATCH 10/18] Revert accidental changes to test-project
---
__fixtures__/test-project/web/src/App.tsx | 10 +---------
.../test-project/web/src/graphql/possibleTypes.ts | 11 -----------
2 files changed, 1 insertion(+), 20 deletions(-)
delete mode 100644 __fixtures__/test-project/web/src/graphql/possibleTypes.ts
diff --git a/__fixtures__/test-project/web/src/App.tsx b/__fixtures__/test-project/web/src/App.tsx
index cb77cb1e4322..65419d60c7d6 100644
--- a/__fixtures__/test-project/web/src/App.tsx
+++ b/__fixtures__/test-project/web/src/App.tsx
@@ -1,7 +1,6 @@
import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web'
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
-import possibleTypes from 'src/graphql/possibleTypes'
import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'
@@ -14,14 +13,7 @@ const App = () => (
-
+
diff --git a/__fixtures__/test-project/web/src/graphql/possibleTypes.ts b/__fixtures__/test-project/web/src/graphql/possibleTypes.ts
deleted file mode 100644
index a8d476e9029c..000000000000
--- a/__fixtures__/test-project/web/src/graphql/possibleTypes.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export interface PossibleTypesResultData {
- possibleTypes: {
- [key: string]: string[]
- }
-}
-
-const result: PossibleTypesResultData = {
- possibleTypes: {},
-}
-
-export default result
From ac6131ba0f588357da20b5fa948598dcad134af2 Mon Sep 17 00:00:00 2001
From: Tobbe Lundberg
Date: Fri, 5 Jan 2024 20:24:51 +0100
Subject: [PATCH 11/18] docs(fragments): Typo, grammar and formatting fixes
(#9802)
---
docs/docs/graphql/fragments.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/docs/graphql/fragments.md b/docs/docs/graphql/fragments.md
index 105ce7af7919..e66041550247 100644
--- a/docs/docs/graphql/fragments.md
+++ b/docs/docs/graphql/fragments.md
@@ -151,7 +151,6 @@ import { fragment } from '@redwoodjs/web/apollo'
Access typename of fragment you registered.
-
```ts
import { typename } from '@redwoodjs/web/apollo'
```
@@ -166,6 +165,7 @@ fragment BookInfo on Book {
author
publicationYear
}
+```
the `typename` is `Book`.
@@ -294,9 +294,9 @@ import possibleTypes from 'src/graphql/possibleTypes'
>
```
-To generate the `src/graphql/possibleTypes`, configure the `redwood.toml`:
+To generate the `src/graphql/possibleTypes` file, enable fragments in `redwood.toml`:
-```toml title=redwood.roml
+```toml title=redwood.toml
[graphql]
- fragments=true
+ fragments = true
```
From ffe7fb867effbad45974608601cbfe636f1f5af6 Mon Sep 17 00:00:00 2001
From: Tom Mrazauskas
Date: Sat, 6 Jan 2024 12:30:18 +0200
Subject: [PATCH 12/18] chore: bump TSTyche (#9803)
---
package.json | 2 +-
packages/router/package.json | 2 +-
packages/web/package.json | 2 +-
yarn.lock | 16 ++++++++--------
4 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/package.json b/package.json
index 5a5ffdd5b298..bdd9d9aec787 100644
--- a/package.json
+++ b/package.json
@@ -104,7 +104,7 @@
"ora": "7.0.1",
"prompts": "2.4.2",
"rimraf": "5.0.5",
- "tstyche": "1.0.0-beta.3",
+ "tstyche": "1.0.0-beta.9",
"tsx": "4.6.2",
"typescript": "5.3.3",
"yargs": "17.7.2",
diff --git a/packages/router/package.json b/packages/router/package.json
index 126c9942b2c3..a1ac88e769dd 100644
--- a/packages/router/package.json
+++ b/packages/router/package.json
@@ -37,7 +37,7 @@
"jest": "29.7.0",
"react": "0.0.0-experimental-e5205658f-20230913",
"react-dom": "0.0.0-experimental-e5205658f-20230913",
- "tstyche": "1.0.0-beta.3",
+ "tstyche": "1.0.0-beta.9",
"typescript": "5.3.3"
},
"peerDependencies": {
diff --git a/packages/web/package.json b/packages/web/package.json
index b1866bb1b94a..0eaee33e96dc 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -61,7 +61,7 @@
"nodemon": "3.0.2",
"react": "0.0.0-experimental-e5205658f-20230913",
"react-dom": "0.0.0-experimental-e5205658f-20230913",
- "tstyche": "1.0.0-beta.3",
+ "tstyche": "1.0.0-beta.9",
"typescript": "5.3.3"
},
"peerDependencies": {
diff --git a/yarn.lock b/yarn.lock
index 56ec480e1eb5..3c992ebdd81e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9320,7 +9320,7 @@ __metadata:
jest: "npm:29.7.0"
react: "npm:0.0.0-experimental-e5205658f-20230913"
react-dom: "npm:0.0.0-experimental-e5205658f-20230913"
- tstyche: "npm:1.0.0-beta.3"
+ tstyche: "npm:1.0.0-beta.9"
typescript: "npm:5.3.3"
peerDependencies:
react: 0.0.0-experimental-e5205658f-20230913
@@ -9642,7 +9642,7 @@ __metadata:
react-hot-toast: "npm:2.4.1"
stacktracey: "npm:2.1.8"
ts-toolbelt: "npm:9.6.0"
- tstyche: "npm:1.0.0-beta.3"
+ tstyche: "npm:1.0.0-beta.9"
typescript: "npm:5.3.3"
peerDependencies:
react: 0.0.0-experimental-e5205658f-20230913
@@ -31801,7 +31801,7 @@ __metadata:
ora: "npm:7.0.1"
prompts: "npm:2.4.2"
rimraf: "npm:5.0.5"
- tstyche: "npm:1.0.0-beta.3"
+ tstyche: "npm:1.0.0-beta.9"
tsx: "npm:4.6.2"
typescript: "npm:5.3.3"
yargs: "npm:17.7.2"
@@ -34452,17 +34452,17 @@ __metadata:
languageName: node
linkType: hard
-"tstyche@npm:1.0.0-beta.3":
- version: 1.0.0-beta.3
- resolution: "tstyche@npm:1.0.0-beta.3"
+"tstyche@npm:1.0.0-beta.9":
+ version: 1.0.0-beta.9
+ resolution: "tstyche@npm:1.0.0-beta.9"
peerDependencies:
typescript: 4.x || 5.x
peerDependenciesMeta:
typescript:
optional: true
bin:
- tstyche: build/bin.js
- checksum: 8db6acde30b8905c891c3854837340e7bbfee20e79e6eae2e674e1e966737f8c79079325c330b51b922f3157a3fcb26da0005032a45ade302f20b1d3e3ccfd37
+ tstyche: ./build/bin.js
+ checksum: 2682c3f7e2d83fa0af795ba14e1c83873e3f8c31f761a8af10512c3476cf824b7ef096ba9deec3fc0e12356beaf2a20abfafcb73202db4f14c7c2877db2c5a87
languageName: node
linkType: hard
From aaf721b02b429caf761e9978c6a795b78e5cc69e Mon Sep 17 00:00:00 2001
From: Tobbe Lundberg
Date: Sat, 6 Jan 2024 11:46:08 +0100
Subject: [PATCH 13/18] Use TS for rebuild-test-project-fixture script (#9804)
---
__fixtures__/test-project/web/package.json | 4 +-
package.json | 2 +-
...ure.js => rebuild-test-project-fixture.ts} | 83 ++++++++-----------
tasks/test-project/typing.js | 40 ---------
tasks/test-project/typing.ts | 21 +++++
5 files changed, 57 insertions(+), 93 deletions(-)
rename tasks/test-project/{rebuild-test-project-fixture.js => rebuild-test-project-fixture.ts} (89%)
delete mode 100644 tasks/test-project/typing.js
create mode 100644 tasks/test-project/typing.ts
diff --git a/__fixtures__/test-project/web/package.json b/__fixtures__/test-project/web/package.json
index d94fbf0cce2f..089dd533bc0c 100644
--- a/__fixtures__/test-project/web/package.json
+++ b/__fixtures__/test-project/web/package.json
@@ -24,9 +24,9 @@
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"autoprefixer": "^10.4.16",
- "postcss": "^8.4.32",
+ "postcss": "^8.4.33",
"postcss-loader": "^7.3.4",
"prettier-plugin-tailwindcss": "0.4.1",
- "tailwindcss": "^3.4.0"
+ "tailwindcss": "^3.4.1"
}
}
diff --git a/package.json b/package.json
index bdd9d9aec787..40df68ed87aa 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
"project:deps": "node ./tasks/framework-tools/frameworkDepsToProject.mjs",
"project:sync": "node ./tasks/framework-tools/frameworkSyncToProject.mjs",
"project:tarsync": "node ./tasks/framework-tools/tarsync.mjs",
- "rebuild-test-project-fixture": "node ./tasks/test-project/rebuild-test-project-fixture.js",
+ "rebuild-test-project-fixture": "tsx ./tasks/test-project/rebuild-test-project-fixture.ts",
"release": "node ./tasks/release/release.mjs",
"release:compare": "node ./tasks/release/compare/compare.mjs",
"release:notes": "node ./tasks/release/generateReleaseNotes.mjs",
diff --git a/tasks/test-project/rebuild-test-project-fixture.js b/tasks/test-project/rebuild-test-project-fixture.ts
similarity index 89%
rename from tasks/test-project/rebuild-test-project-fixture.js
rename to tasks/test-project/rebuild-test-project-fixture.ts
index 32dee47c1bfb..9035328331bb 100755
--- a/tasks/test-project/rebuild-test-project-fixture.js
+++ b/tasks/test-project/rebuild-test-project-fixture.ts
@@ -1,34 +1,28 @@
-#!/usr/bin/env node
-/* eslint-env node, es6*/
-//@ts-check
-const fs = require('fs')
-const os = require('os')
-const path = require('path')
-
-const chalk = require('chalk')
-const fse = require('fs-extra')
-const { rimraf } = require('rimraf')
-const { hideBin } = require('yargs/helpers')
-const yargs = require('yargs/yargs')
-
-const {
- RedwoodTUI,
- ReactiveTUIContent,
- RedwoodStyling,
-} = require('@redwoodjs/tui')
-
-const {
+import fs from 'node:fs'
+import os from 'node:os'
+import path from 'node:path'
+
+import chalk from 'chalk'
+import fse from 'fs-extra'
+import { rimraf } from 'rimraf'
+import { hideBin } from 'yargs/helpers'
+import yargs from 'yargs/yargs'
+
+import { RedwoodTUI, ReactiveTUIContent, RedwoodStyling } from '@redwoodjs/tui'
+
+import {
addFrameworkDepsToProject,
copyFrameworkPackages,
-} = require('./frameworkLinking')
-const { webTasks, apiTasks } = require('./tui-tasks')
-const { isAwaitable } = require('./typing')
-const {
- getExecaOptions: utilGetExecaOptions,
+} from './frameworkLinking'
+import { webTasks, apiTasks } from './tui-tasks'
+import { isAwaitable } from './typing'
+import type { TuiTaskDef } from './typing'
+import {
+ getExecaOptions as utilGetExecaOptions,
updatePkgJsonScripts,
ExecaError,
exec,
-} = require('./util')
+} from './util'
const args = yargs(hideBin(process.argv))
.usage('Usage: $0 [option]')
@@ -55,6 +49,7 @@ const args = yargs(hideBin(process.argv))
const { verbose, resume, resumePath, resumeStep } = args
+const RW_FRAMEWORK_PATH = path.join(__dirname, '../../')
const OUTPUT_PROJECT_PATH = resumePath
? /* path.resolve(String(resumePath)) */ resumePath
: path.join(
@@ -82,27 +77,18 @@ if (!startStep) {
}
}
-const RW_FRAMEWORKPATH = path.join(__dirname, '../../')
-
const tui = new RedwoodTUI()
-/** @type {(string) => import('execa').Options} */
-function getExecaOptions(cwd) {
+function getExecaOptions(cwd: string) {
return { ...utilGetExecaOptions(cwd), stdio: 'pipe' }
}
-/**
- * @param {string} step
- */
-function beginStep(step) {
+function beginStep(step: string) {
fs.mkdirSync(OUTPUT_PROJECT_PATH, { recursive: true })
fs.writeFileSync(path.join(OUTPUT_PROJECT_PATH, 'step.txt'), '' + step)
}
-/**
- * @param {import('./typing').TuiTaskDef} taskDef
- */
-async function tuiTask({ step, title, content, task, parent }) {
+async function tuiTask({ step, title, content, task, parent }: TuiTaskDef) {
const stepId = (parent ? parent + '.' : '') + step
const tuiContent = new ReactiveTUIContent({
@@ -139,7 +125,7 @@ async function tuiTask({ step, title, content, task, parent }) {
return
}
- let promise
+ let promise: void | Promise
try {
promise = task()
@@ -251,28 +237,25 @@ if (resumePath && !fs.existsSync(path.join(resumePath, 'redwood.toml'))) {
}
const createProject = () => {
- let cmd = `yarn node ./packages/create-redwood-app/dist/create-redwood-app.js ${OUTPUT_PROJECT_PATH}`
+ const cmd = `yarn node ./packages/create-redwood-app/dist/create-redwood-app.js ${OUTPUT_PROJECT_PATH}`
const subprocess = exec(
cmd,
// We create a ts project and convert using ts-to-js at the end if typescript flag is false
['--no-yarn-install', '--typescript', '--overwrite', '--no-git'],
- getExecaOptions(RW_FRAMEWORKPATH)
+ getExecaOptions(RW_FRAMEWORK_PATH)
)
return subprocess
}
const copyProject = async () => {
- const FIXTURE_TESTPROJ_PATH = path.join(
- RW_FRAMEWORKPATH,
- '__fixtures__/test-project'
- )
+ const fixturePath = path.join(RW_FRAMEWORK_PATH, '__fixtures__/test-project')
// remove existing Fixture
- await rimraf(FIXTURE_TESTPROJ_PATH)
+ await rimraf(fixturePath)
// copy from tempDir to Fixture dir
- await fse.copy(OUTPUT_PROJECT_PATH, FIXTURE_TESTPROJ_PATH)
+ await fse.copy(OUTPUT_PROJECT_PATH, fixturePath)
// cleanup after ourselves
await rimraf(OUTPUT_PROJECT_PATH)
}
@@ -304,7 +287,7 @@ async function runCommand() {
return exec(
'yarn build:clean && yarn build',
[],
- getExecaOptions(RW_FRAMEWORKPATH)
+ getExecaOptions(RW_FRAMEWORK_PATH)
)
},
})
@@ -315,7 +298,7 @@ async function runCommand() {
content: 'Adding framework dependencies to project...',
task: () => {
return addFrameworkDepsToProject(
- RW_FRAMEWORKPATH,
+ RW_FRAMEWORK_PATH,
OUTPUT_PROJECT_PATH,
'pipe' // TODO: Remove this when everything is using @rwjs/tui
)
@@ -362,7 +345,7 @@ async function runCommand() {
title: '[link] Copying framework packages to project',
task: () => {
return copyFrameworkPackages(
- RW_FRAMEWORKPATH,
+ RW_FRAMEWORK_PATH,
OUTPUT_PROJECT_PATH,
'pipe'
)
diff --git a/tasks/test-project/typing.js b/tasks/test-project/typing.js
deleted file mode 100644
index cc9d81f96634..000000000000
--- a/tasks/test-project/typing.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * @typedef ExecaResult
- * @type {object}
- * @property {string} stdout
- * @property {string} stderr
- * @property {number} exitCode
- */
-
-/**
- * @typedef TuiTaskDef
- * @type {object}
- * @property {number} step 0 based step number.
- * @property {string=} parent The parent task to this task.
- * @property {string} title Title of this task.
- * @property {string=} content Reactive content.
- * @property {() => boolean=} enabled Whether this task is enabled or not.
- * Disabled tasks don't show up in the list.
- * @property {() => Promise | void} task
- * The task to run. Will be passed an instance of TUI when called.
- */
-
-/**
- * @typedef TuiTaskList
- * @type {Array>}
- */
-
-/**
- * @param {Promise | void} promise
- * @return {promise is Promise}
- */
-function isAwaitable(promise) {
- return (
- typeof promise !== 'undefined' &&
- 'then' in /** @type Promise */ (promise)
- )
-}
-
-module.exports = {
- isAwaitable,
-}
diff --git a/tasks/test-project/typing.ts b/tasks/test-project/typing.ts
new file mode 100644
index 000000000000..9528b57ef432
--- /dev/null
+++ b/tasks/test-project/typing.ts
@@ -0,0 +1,21 @@
+export interface TuiTaskDef {
+ /** 0 based step number */
+ step: number
+ /** The parent task to this task. */
+ parent?: string
+ /** Title of this task. */
+ title: string
+ /** Reactive content */
+ content?: string
+ /**
+ * Whether this task is enabled or not. Disabled tasks don't show up in the
+ * list
+ */
+ enabled?: boolean | (() => boolean)
+ /** The task to run. Will be passed an instance of TUI when called */
+ task: () => Promise | void
+}
+
+export function isAwaitable(promise: unknown): promise is Promise {
+ return typeof promise !== 'undefined' && 'then' in promise
+}
From 8466e752f637403c4f0b440928385ca51d136bcf Mon Sep 17 00:00:00 2001
From: Tobbe Lundberg
Date: Sat, 6 Jan 2024 13:20:00 +0100
Subject: [PATCH 14/18] chore(ci): Update task names to say "node 20" (#9805)
---
.github/workflows/ci.yml | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 61c51cbebd7a..336f8f802a1f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -90,7 +90,7 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest]
- name: π Build, lint, test / ${{ matrix.os }} / node 18 latest
+ name: π Build, lint, test / ${{ matrix.os }} / node 20 latest
runs-on: ${{ matrix.os }}
steps:
@@ -144,7 +144,7 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest]
- name: π Build, lint, test / ${{ matrix.os }} / node 18 latest
+ name: π Build, lint, test / ${{ matrix.os }} / node 20 latest
runs-on: ${{ matrix.os }}
steps:
@@ -157,7 +157,7 @@ jobs:
matrix:
bundler: [vite, webpack]
- name: π² Tutorial E2E / ${{ matrix.bundler }} / node 18 latest
+ name: π² Tutorial E2E / ${{ matrix.bundler }} / node 20 latest
runs-on: ubuntu-latest
steps:
@@ -235,7 +235,7 @@ jobs:
matrix:
bundler: [vite, webpack]
- name: π² Tutorial E2E / ${{ matrix.bundler }} / node 18 latest
+ name: π² Tutorial E2E / ${{ matrix.bundler }} / node 20 latest
runs-on: ubuntu-latest
steps:
@@ -249,7 +249,7 @@ jobs:
os: [ubuntu-latest, windows-latest]
bundler: [vite, webpack]
- name: π Smoke tests / ${{ matrix.os }} / ${{ matrix.bundler }} / node 18 latest
+ name: π Smoke tests / ${{ matrix.os }} / ${{ matrix.bundler }} / node 20 latest
runs-on: ${{ matrix.os }}
env:
@@ -427,7 +427,7 @@ jobs:
#
# ```
# env:
- # RECORD_REPLAY_METADATA_TEST_RUN_TITLE: π Smoke tests / ${{ matrix.os }} / node 18 latest
+ # RECORD_REPLAY_METADATA_TEST_RUN_TITLE: π Smoke tests / ${{ matrix.os }} / node 20 latest
# RECORD_REPLAY_TEST_METRICS: 1
# ```
#
@@ -446,7 +446,7 @@ jobs:
os: [ubuntu-latest, windows-latest]
bundler: [vite, webpack]
- name: π Smoke tests / ${{ matrix.os }} / ${{ matrix.bundler }} / node 18 latest
+ name: π Smoke tests / ${{ matrix.os }} / ${{ matrix.bundler }} / node 20 latest
runs-on: ${{ matrix.os }}
steps:
@@ -459,7 +459,7 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest]
- name: π Telemetry check / ${{ matrix.os }} / node 18 latest
+ name: π Telemetry check / ${{ matrix.os }} / node 20 latest
runs-on: ${{ matrix.os }}
env:
@@ -506,7 +506,7 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest]
- name: π Telemetry check / ${{ matrix.os }} / node 18 latest
+ name: π Telemetry check / ${{ matrix.os }} / node 20 latest
runs-on: ${{ matrix.os }}
steps:
From 6fbc5b837c1a19d6989dca8480f1abfe48d5f326 Mon Sep 17 00:00:00 2001
From: Tobbe Lundberg
Date: Sat, 6 Jan 2024 14:33:01 +0100
Subject: [PATCH 15/18] chore(cli): More robust isAwaitable (#9806)
---
tasks/test-project/typing.ts | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/tasks/test-project/typing.ts b/tasks/test-project/typing.ts
index 9528b57ef432..d4b15fb30e89 100644
--- a/tasks/test-project/typing.ts
+++ b/tasks/test-project/typing.ts
@@ -17,5 +17,10 @@ export interface TuiTaskDef {
}
export function isAwaitable(promise: unknown): promise is Promise {
- return typeof promise !== 'undefined' && 'then' in promise
+ return (
+ !!promise &&
+ typeof promise === 'object' &&
+ 'then' in promise &&
+ typeof promise.then === 'function'
+ )
}
From 93b04f8583b3b78a1f33195248bb7e3ebc4cd3bc Mon Sep 17 00:00:00 2001
From: Dominic Saadi
Date: Sat, 6 Jan 2024 15:03:44 -0800
Subject: [PATCH 16/18] chore(esm): convert crwa to esm and bundle (#9786)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Follow up to https://github.com/redwoodjs/redwood/pull/9783. This PR
converts the `create-redwood-app` package to ESM and bundles all its
dependencies. I started with `create-redwood-app` because the
requirements for making it ESM were relatively trivial compared to the
other packages since it just needs to be run by `yarn create`, and yarn
create just runs a bin.
For `create-redwood-app` I'm just using esbuild. Why not use tsup?
1. We're just distributing a bin. We're not distributing a dual-module
package with types
2. tsup hasn't been committed to in over a month and a half, whereas
esbuild releases often and is committed to more-or-less daily. That
doesn't automatically disqualify it (OSS is hard), but makes me wary.
I'll consider it for packages that distribute more than a bin
More on bundling. Bundling this package has benefits, namely decreasing
the install timeβyarn doesn't have to fetch its dependencies, there are
none. But I'm mostly doing it as an exercise because we need to do it
more. For background on the shims (`jsBanner`) see
https://github.com/evanw/esbuild/issues/1921. It's nothing bespoke and
it's what tsup[^tsup] and Vite[^vite] would've done anyway.
Other notes:
- Updates the package's `README.md`; this could be updated more but I
didn't want to spend too much time on it
- Adds e2e tests for the node version check
Two new e2e tests make sure we're checking node version correctly. I
can't use nvm since 1. it's not easily scriptable (it's a shell built-in
or something) and 2. it doesn't seem like we all use it, so I just added
these tests to CI and use the GitHub action to change the node version
- Fixes a bug I introduced in
https://github.com/redwoodjs/redwood/pull/9728
The node version check would throw if it didn't pass because
`engines.yarn` was removed. This wasn't released
- Converted files to just `.js` since Node recognized them as ESM from
`type` in `package.json`
- Reordered `yarn create-redwood-app`'s options in help; I tried to put
the ones that were more likely to be used first
- Removed the header from `--help` and `--version`
[^tsup]:
https://github.com/egoist/tsup/blob/8c26e63c92711d60c05aedd3cdc358530ba266c5/assets/esm_shims.js
[^vite]:
https://github.com/vitejs/vite/blob/8de7bd2b68db27b83d9484cc8d4e26436615168e/packages/vite/rollup.config.ts#L288-L295
---
.github/workflows/ci.yml | 26 ++-
packages/create-redwood-app/README.md | 72 ++++---
packages/create-redwood-app/build.mjs | 23 ---
packages/create-redwood-app/jest.config.js | 5 +-
packages/create-redwood-app/package.json | 23 ++-
packages/create-redwood-app/scripts/build.js | 34 ++++
...tUpTestProject.mjs => setUpTestProject.js} | 0
packages/create-redwood-app/scripts/tsToJS.js | 113 +++++++++++
.../create-redwood-app/scripts/tsToJS.mjs | 126 -------------
.../src/create-redwood-app.js | 175 +++++++-----------
packages/create-redwood-app/src/telemetry.js | 2 +-
.../tests/{e2e.test.mjs => e2e.test.js} | 35 ++--
.../create-redwood-app/tests/e2e_prompts.sh | 4 +-
.../tests/e2e_prompts_git.sh | 2 +-
.../create-redwood-app/tests/e2e_prompts_m.sh | 4 +-
.../tests/e2e_prompts_node_greater.sh | 48 +++++
.../tests/e2e_prompts_node_less.sh | 30 +++
.../tests/e2e_prompts_overwrite.sh | 6 +-
.../tests/e2e_prompts_ts.sh | 2 +-
.../{templates.test.mjs => templates.test.js} | 4 +-
20 files changed, 385 insertions(+), 349 deletions(-)
delete mode 100644 packages/create-redwood-app/build.mjs
create mode 100644 packages/create-redwood-app/scripts/build.js
rename packages/create-redwood-app/scripts/{setUpTestProject.mjs => setUpTestProject.js} (100%)
create mode 100644 packages/create-redwood-app/scripts/tsToJS.js
delete mode 100644 packages/create-redwood-app/scripts/tsToJS.mjs
rename packages/create-redwood-app/tests/{e2e.test.mjs => e2e.test.js} (74%)
create mode 100755 packages/create-redwood-app/tests/e2e_prompts_node_greater.sh
create mode 100755 packages/create-redwood-app/tests/e2e_prompts_node_less.sh
rename packages/create-redwood-app/tests/{templates.test.mjs => templates.test.js} (99%)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 336f8f802a1f..923978293a20 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -750,11 +750,13 @@ jobs:
env:
PROJECT_PATH: ${{ env.PROJECT_PATH }}
- - name: Prompt tests
+ - name: Install expect
run: |
sudo apt-get update
sudo apt-get install expect
+ - name: Prompt tests
+ run: |
./tests/e2e_prompts.sh
./tests/e2e_prompts_git.sh
./tests/e2e_prompts_m.sh
@@ -764,6 +766,28 @@ jobs:
env:
PROJECT_PATH: ${{ env.PROJECT_PATH }}
+ - name: β¬’ Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 18
+
+ - name: Prompt tests
+ run: ./tests/e2e_prompts_node_less.sh
+ working-directory: ./packages/create-redwood-app
+ env:
+ PROJECT_PATH: ${{ env.PROJECT_PATH }}
+
+ - name: β¬’ Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 21
+
+ - name: Prompt tests
+ run: ./tests/e2e_prompts_node_greater.sh
+ working-directory: ./packages/create-redwood-app
+ env:
+ PROJECT_PATH: ${{ env.PROJECT_PATH }}
+
crwa-skip:
needs: detect-changes
if: needs.detect-changes.outputs.onlydocs == 'true'
diff --git a/packages/create-redwood-app/README.md b/packages/create-redwood-app/README.md
index ea65f31b707d..3719968f56f5 100644
--- a/packages/create-redwood-app/README.md
+++ b/packages/create-redwood-app/README.md
@@ -18,39 +18,35 @@
Ship today with architecture for tomorrow.
-Redwood is an opinionated, edge-ready framework for modern multi-client applications, built on React, GraphQL, and Prisma with full TypeScript support and ready to go with zero config.
+Redwood is an opinionated framework for modern multi-client applications, built on React, GraphQL, and Prisma with full TypeScript support and ready to go with zero config.
Want great developer experience and easy scaling? How about an integrated front- and back-end test suite, boilerplate code generators, component design, logging, API security + auth, and serverless or traditional deploy support? Redwood is here! Redwood works with the components and development workflow you love but with simple conventions and helpers to make your experience even better.
-
-
-- The [Redwood Tutorial](https://redwoodjs.com/docs/tutorial): The best way to learn Redwood
+- The [Redwood Tutorial](https://redwoodjs.com/docs/tutorial): the best way to learn Redwood
- The [Redwood CLI](https://redwoodjs.com/docs/cli-commands): code generators, DB helpers, setup commands, and more
- [Documentation](https://redwoodjs.com/docs) and [How To's](https://redwoodjs.com/how-to/custom-function)
- Join the Community [Forums](https://community.redwoodjs.com) and [Chat](https://discord.gg/redwoodjs)
-
-
-
Contributing to create-redwood-app
+
Contributing to create-redwood-app
-_Contributors are Welcome! Get started [here](https://redwoodjs.com/docs/contributing). And don't hesitate to ask for help on the forums and chat_
+_Contributors are Welcome! Get started [here](https://redwoodjs.com/docs/contributing). And don't hesitate to ask for help on the forums and chat_.
**Table of Contents**
- [Description](#description)
-- [Package Leads](#package-leads)
-- [Roadmap](#roadmap)
- [Local Development](#local-development)
- [Installation Script](#installation-script)
- [Template Codebase](#template-codebase)
@@ -60,55 +56,45 @@ _Contributors are Welcome! Get started [here](https://redwoodjs.com/docs/contrib
## Description
This package creates and installs a Redwood project, which is the entry point for anyone using Redwood. It has two parts:
-- The installation script `create-redwood-app.js`
-- Project template code in the `template/` directory
-
-> _For information about contributing to the Redwood Framework in general, [please start here](https://redwoodjs.com/docs/contributing)._
-
-## Package Leads
-- [@peterp](https://github.com/peterp)
-- [@thedavidprice](https://github.com/thedavidprice)
-
-## Roadmap
-
-v1 Priorities:
-- convert `template/` codebase to TypeScript
-- add option to install as either TypeScript or JavaScript project (defaults to TypeScript)
-- add package tests, which may be accomplished by including in Cypress E2E CI
+- The installation script [`src/create-redwood-app.js`](./src/create-redwood-app.js)
+- Project template code in the [`templates/`](./templates/) directory
## Local Development
### Installation Script
-The installation script is built with [Yargs](https://github.com/yargs/yargs)
+
+The installation script is built with [Yargs](https://github.com/yargs/yargs).
### Template Codebase
-The project codebase in `template/` uses [Yarn Workspace v1](https://classic.yarnpkg.com/en/docs/workspaces/) for a monorepo project containing the API and Web Sides. Redwood packages are included in `template/package.json`, `template/web/package.json`, and `template/api/package.json`, respectively.
-### How to run create-redwood-app from your local repo and create a project
+The project codebase in [`templates/`](./templates/) uses [Yarn Workspaces](https://yarnpkg.com/features/workspaces) for a monorepo project containing the API and Web Sides. Redwood packages are included in `templates/ts/package.json`, `templates/ts/web/package.json`, and `templates/ts/api/package.json`, respectively.
+
+### How to run `create-redwood-app` from your local repo and create a project
+
First, run the following commands in the root of the monorepo:
+
```bash
yarn install
yarn build
```
-Then, we need to navigate to the create redwood app package and build the script:
+Then, navigate to the create redwood app package:
+
```bash
cd packages/create-redwood-app
-yarn build
```
-_Note:_ You can also use `yarn build:watch` instead of `yarn build` to watch for changes and rebuild automatically.
-
-This will generate the `create-redwood-app.js` file inside the `dist` directory.
+Run `yarn node` on the built file (`dist/create-redwood-app.js`) and pass in the path to the new project:
-To use the script, run `node` on that file (dist/create-redwood-app.js) and pass in the path to the new project:
```bash
-node dist/create-redwood-app.js /path/to/new/redwood-app
+yarn node ./dist/create-redwood-app.js /path/to/new/redwood-app
```
-> Note: the new project will install with the most recent major Redwood package version by default
+> [!NOTE]
+> the new project will install with the most recent major Redwood package version by default.
### How to run other published versions for debugging
+
By default yarn create will pick the latest stable version to run, but you can specify a different version via yarn too!
To try the canary version, run:
@@ -121,12 +107,16 @@ Note that this will still create a project with the latest stable version, but r
You can specify any tag or version instead of `@canary`
### Develop using the new project
+
There are three options for developing with the installed project:
**1. Upgrade the project to use the latest canary release**
+
```bash
cd /path/to/new/redwood-app
yarn rw upgrade -t canary
```
+
**2. Use the workflow and tools for local package development**
-- [Local Development Instructions](https://github.com/redwoodjs/redwood/blob/main/CONTRIBUTING.md#local-development)
+
+- [Local Development Instructions](https://github.com/redwoodjs/redwood/blob/main/CONTRIBUTING.md)
diff --git a/packages/create-redwood-app/build.mjs b/packages/create-redwood-app/build.mjs
deleted file mode 100644
index 3ed59c69cd9b..000000000000
--- a/packages/create-redwood-app/build.mjs
+++ /dev/null
@@ -1,23 +0,0 @@
-import fs from 'node:fs'
-
-import * as esbuild from 'esbuild'
-
-const result = await esbuild.build({
- entryPoints: ['src/create-redwood-app.js'],
- outfile: 'dist/create-redwood-app.js',
-
- bundle: true,
- minify: true,
-
- platform: 'node',
- target: ['node20'],
- packages: 'external',
-
- logLevel: 'info',
-
- // For visualizing the bundle.
- // See https://esbuild.github.io/api/#metafile and https://esbuild.github.io/analyze/.
- metafile: true,
-})
-
-fs.writeFileSync('meta.json', JSON.stringify(result.metafile))
diff --git a/packages/create-redwood-app/jest.config.js b/packages/create-redwood-app/jest.config.js
index 2c638b714e5b..99951968fbc0 100644
--- a/packages/create-redwood-app/jest.config.js
+++ b/packages/create-redwood-app/jest.config.js
@@ -1,8 +1,5 @@
/** @type {import('jest').Config} */
-const config = {
- testMatch: ['/tests/*.test.mjs'],
+export default {
testPathIgnorePatterns: ['/node_modules/', '/templates/'],
transform: {},
}
-
-module.exports = config
diff --git a/packages/create-redwood-app/package.json b/packages/create-redwood-app/package.json
index e90e7e89148e..1ea46667c57b 100644
--- a/packages/create-redwood-app/package.json
+++ b/packages/create-redwood-app/package.json
@@ -7,34 +7,41 @@
"directory": "packages/create-redwood-app"
},
"license": "MIT",
+ "type": "module",
"bin": "./dist/create-redwood-app.js",
"files": [
"dist",
"templates"
],
"scripts": {
- "build": "yarn node ./build.mjs",
+ "build": "node ./scripts/build.js",
"build:pack": "yarn pack -o create-redwood-app.tgz",
"build:watch": "nodemon --watch src --ignore dist,template --exec \"yarn build\"",
"prepublishOnly": "NODE_ENV=production yarn build",
- "set-up-test-project": "node ./scripts/setUpTestProject.mjs",
+ "set-up-test-project": "node ./scripts/setUpTestProject.js",
"test": "node --experimental-vm-modules $(yarn bin jest) templates",
"test:e2e": "node --experimental-vm-modules $(yarn bin jest) e2e",
- "ts-to-js": "yarn node ./scripts/tsToJS.mjs"
+ "ts-to-js": "yarn node ./scripts/tsToJS.js"
},
- "dependencies": {
+ "devDependencies": {
+ "@babel/core": "^7.22.20",
+ "@babel/plugin-transform-typescript": "^7.22.15",
"@opentelemetry/api": "1.7.0",
"@opentelemetry/exporter-trace-otlp-http": "0.45.1",
"@opentelemetry/resources": "1.18.1",
"@opentelemetry/sdk-trace-node": "1.18.1",
"@opentelemetry/semantic-conventions": "1.18.1",
"@redwoodjs/tui": "6.0.7",
+ "@types/babel__core": "7.20.4",
"chalk": "4.1.2",
"check-node-version": "4.2.1",
"ci-info": "4.0.0",
"envinfo": "7.11.0",
+ "esbuild": "0.19.9",
"execa": "5.1.1",
"fs-extra": "11.2.0",
+ "jest": "29.7.0",
+ "klaw-sync": "6.0.0",
"semver": "7.5.4",
"systeminformation": "5.21.20",
"terminal-link": "2.1.1",
@@ -42,13 +49,5 @@
"uuid": "9.0.1",
"yargs": "17.7.2"
},
- "devDependencies": {
- "@babel/core": "^7.22.20",
- "@babel/plugin-transform-typescript": "^7.22.15",
- "@types/babel__core": "7.20.4",
- "esbuild": "0.19.9",
- "jest": "29.7.0",
- "klaw-sync": "6.0.0"
- },
"gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1"
}
diff --git a/packages/create-redwood-app/scripts/build.js b/packages/create-redwood-app/scripts/build.js
new file mode 100644
index 000000000000..6694591a9998
--- /dev/null
+++ b/packages/create-redwood-app/scripts/build.js
@@ -0,0 +1,34 @@
+/* eslint-env node */
+
+import * as esbuild from 'esbuild'
+import fs from 'fs-extra'
+
+const jsBanner = `\
+#!/usr/bin/env node
+
+const require = (await import("node:module")).createRequire(import.meta.url);
+const __filename = (await import("node:url")).fileURLToPath(import.meta.url);
+const __dirname = (await import("node:path")).dirname(__filename);
+`
+
+const result = await esbuild.build({
+ entryPoints: ['src/create-redwood-app.js'],
+ outdir: 'dist',
+
+ platform: 'node',
+ target: ['node20'],
+ format: 'esm',
+ bundle: true,
+ banner: {
+ js: jsBanner,
+ },
+
+ minify: true,
+
+ logLevel: 'info',
+ metafile: true,
+})
+
+await fs.writeJSON(new URL('./meta.json', import.meta.url), result.metafile, {
+ spaces: 2,
+})
diff --git a/packages/create-redwood-app/scripts/setUpTestProject.mjs b/packages/create-redwood-app/scripts/setUpTestProject.js
similarity index 100%
rename from packages/create-redwood-app/scripts/setUpTestProject.mjs
rename to packages/create-redwood-app/scripts/setUpTestProject.js
diff --git a/packages/create-redwood-app/scripts/tsToJS.js b/packages/create-redwood-app/scripts/tsToJS.js
new file mode 100644
index 000000000000..7e5c881330d9
--- /dev/null
+++ b/packages/create-redwood-app/scripts/tsToJS.js
@@ -0,0 +1,113 @@
+/* eslint-env node */
+
+import { fileURLToPath } from 'node:url'
+
+import { transformFileSync } from '@babel/core'
+import { format } from 'prettier'
+import { fs, glob, path } from 'zx'
+
+const TS_TEMPLATE_PATH = fileURLToPath(
+ new URL('../templates/ts', import.meta.url)
+)
+
+// Remove `node_modules`, `.yarn/install-state.gz`.
+console.log('Removing `node_modules` in the TS template')
+const tsTemplateNodeModulesPath = path.join(TS_TEMPLATE_PATH, 'node_modules')
+await fs.rm(tsTemplateNodeModulesPath, { recursive: true, force: true })
+
+console.log("Removing yarn's `install-state.gz` in the TS template")
+const tsTemplateYarnInstallStatePath = path.join(
+ TS_TEMPLATE_PATH,
+ '.yarn',
+ 'install-state.gz'
+)
+await fs.rm(tsTemplateYarnInstallStatePath, { force: true })
+
+// Clean and copy the TS template to the JS template.
+const JS_TEMPLATE_PATH = fileURLToPath(
+ new URL('../templates/js', import.meta.url)
+)
+
+console.log('Removing the JS template')
+await fs.rm(JS_TEMPLATE_PATH, { recursive: true, force: true })
+console.log('Copying the TS template to the JS template')
+await fs.copy(TS_TEMPLATE_PATH, JS_TEMPLATE_PATH)
+
+// Find files and transform.
+const filePaths = await glob(['{api,web,scripts}/**/*.{ts,tsx}'], {
+ cwd: JS_TEMPLATE_PATH,
+ absolute: true,
+})
+
+console.group('Transforming files in the JS template')
+
+const { default: prettierConfig } = await import(
+ new URL('../templates/ts/prettier.config.js', import.meta.url)
+)
+
+for (const filePath of filePaths) {
+ console.log(`β’ ${filePath}`)
+
+ const result = transformFileSync(filePath, {
+ cwd: TS_TEMPLATE_PATH,
+ configFile: false,
+ plugins: [
+ [
+ '@babel/plugin-transform-typescript',
+ {
+ isTSX: true,
+ allExtensions: true,
+ },
+ ],
+ ],
+ retainLines: true,
+ })
+
+ if (!result) {
+ throw new Error(`Error: Couldn't transform ${filePath}`)
+ }
+
+ const formattedCode = format(result.code, {
+ ...prettierConfig,
+ parser: 'babel',
+ })
+
+ await fs.writeFile(
+ filePath.replace('.tsx', '.jsx').replace('.ts', '.js'),
+ formattedCode,
+ 'utf-8'
+ )
+
+ await fs.rm(filePath)
+}
+
+console.groupEnd()
+
+console.group(
+ 'Transforming `tsconfig.json`s in the JS template to `jsconfig.json`s'
+)
+
+const tsConfigFilePaths = await glob(['{api,web,scripts}/**/tsconfig.json'], {
+ cwd: JS_TEMPLATE_PATH,
+ absolute: true,
+})
+
+for (const tsConfigFilePath of tsConfigFilePaths) {
+ console.log(`β’ ${tsConfigFilePath}`)
+
+ const jsConfigFilePath = path.join(
+ path.dirname(tsConfigFilePath),
+ 'jsconfig.json'
+ )
+
+ await fs.rename(tsConfigFilePath, jsConfigFilePath)
+
+ const jsConfig = await fs.readJSON(jsConfigFilePath)
+
+ // This property has no meaning in JS projects.
+ delete jsConfig.compilerOptions.allowJs
+
+ await fs.writeJSON(jsConfigFilePath, jsConfig, { spaces: 2 })
+}
+
+console.groupEnd()
diff --git a/packages/create-redwood-app/scripts/tsToJS.mjs b/packages/create-redwood-app/scripts/tsToJS.mjs
deleted file mode 100644
index 990386cc8600..000000000000
--- a/packages/create-redwood-app/scripts/tsToJS.mjs
+++ /dev/null
@@ -1,126 +0,0 @@
-#!/usr/bin/env node
-/* eslint-env node */
-
-import path from 'node:path'
-import { fileURLToPath } from 'node:url'
-
-import { transformFileSync } from '@babel/core'
-import fg from 'fast-glob'
-import fs from 'fs-extra'
-import { format } from 'prettier'
-
-const [TS_TEMPLATE_FILEPATH, JS_TEMPLATE_FILEPATH] = [
- new URL('../templates/ts', import.meta.url),
- new URL('../templates/js', import.meta.url),
-].map(fileURLToPath)
-
-const { default: prettierConfig } = await import(
- new URL('../templates/ts/prettier.config.js', import.meta.url)
-)
-
-// Handle node_modules, .yarn/install-state.gz.
-const tsTemplateNodeModulesPath = path.join(
- TS_TEMPLATE_FILEPATH,
- 'node_modules'
-)
-
-if (fs.existsSync(tsTemplateNodeModulesPath)) {
- console.log('Removing node modules in TS template')
- fs.rmSync(tsTemplateNodeModulesPath, { recursive: true })
-}
-
-const tsTemplateYarnInstallState = path.join(
- TS_TEMPLATE_FILEPATH,
- '.yarn',
- 'install-state.gz'
-)
-
-if (fs.existsSync(tsTemplateYarnInstallState)) {
- console.log('Removing .yarn/install-state.gz in TS template')
- fs.rmSync(tsTemplateYarnInstallState, { recursive: true })
-}
-
-// Clean and copy the TS template to the JS template.
-console.log('Cleaning JS template')
-fs.rmSync(JS_TEMPLATE_FILEPATH, { recursive: true })
-
-console.log('Copying TS template to JS template')
-fs.copySync(TS_TEMPLATE_FILEPATH, JS_TEMPLATE_FILEPATH)
-
-// Find files and transform.
-const apiWebFilePaths = fg.sync('{api,web}/**/*.{ts,tsx}', {
- cwd: JS_TEMPLATE_FILEPATH,
- absolute: true,
-})
-
-const scriptFilePaths = fg.sync('scripts/**/*.ts', {
- cwd: JS_TEMPLATE_FILEPATH,
- absolute: true,
-})
-
-console.group('Transforming TS files in JS template to JS')
-
-for (const filePath of [...apiWebFilePaths, ...scriptFilePaths]) {
- console.log('Transforming', filePath)
-
- const result = transformFileSync(filePath, {
- cwd: TS_TEMPLATE_FILEPATH,
- configFile: false,
- plugins: [
- [
- '@babel/plugin-transform-typescript',
- {
- isTSX: true,
- allExtensions: true,
- },
- ],
- ],
- retainLines: true,
- })
-
- if (!result) {
- throw new Error(`Babel transform for ${filePath} failed`)
- }
-
- const formattedCode = format(result.code, {
- ...prettierConfig,
- parser: 'babel',
- })
-
- fs.writeFileSync(
- filePath.replace('.tsx', '.jsx').replace('.ts', '.js'),
- formattedCode,
- 'utf-8'
- )
-
- fs.rmSync(filePath)
-}
-
-console.groupEnd()
-
-console.group('Transforming tsconfig files to jsconfig')
-
-const tsConfigFilePaths = fg.sync('{api,web,scripts}/**/tsconfig.json', {
- cwd: JS_TEMPLATE_FILEPATH,
- absolute: true,
-})
-
-for (const tsConfigFilePath of tsConfigFilePaths) {
- console.log('Transforming', tsConfigFilePath)
-
- const jsConfigFilePath = path.join(
- path.dirname(tsConfigFilePath),
- 'jsconfig.json'
- )
-
- fs.renameSync(tsConfigFilePath, jsConfigFilePath)
-
- const jsConfig = fs.readJSONSync(jsConfigFilePath)
-
- // This property has no meaning in JS projects.
- delete jsConfig.compilerOptions.allowJs
-
- fs.writeJSONSync(jsConfigFilePath, jsConfig, { spaces: 2 })
-}
-
-console.groupEnd()
diff --git a/packages/create-redwood-app/src/create-redwood-app.js b/packages/create-redwood-app/src/create-redwood-app.js
index 2f7e30405aeb..71817907bea5 100644
--- a/packages/create-redwood-app/src/create-redwood-app.js
+++ b/packages/create-redwood-app/src/create-redwood-app.js
@@ -1,6 +1,5 @@
-#!/usr/bin/env node
-
-import path from 'path'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
import { trace, SpanStatusCode } from '@opentelemetry/api'
import checkNodeVersionCb from 'check-node-version'
@@ -14,19 +13,28 @@ import yargs from 'yargs/yargs'
import { RedwoodTUI, ReactiveTUIContent, RedwoodStyling } from '@redwoodjs/tui'
-import { name, version } from '../package'
+import { name, version } from '../package.json'
import {
UID,
startTelemetry,
shutdownTelemetry,
recordErrorViaTelemetry,
-} from './telemetry'
+} from './telemetry.js'
const INITIAL_COMMIT_MESSAGE = 'Initial commit'
-// Telemetry
-const { telemetry } = Parser(hideBin(process.argv))
+// Telemetry can be disabled in two ways:
+// - by passing `--telemetry false` or `--no-telemetry`
+// - by setting the `REDWOOD_DISABLE_TELEMETRY` env var to `1`
+const { telemetry } = Parser(hideBin(process.argv), {
+ boolean: ['telemetry'],
+ default: {
+ telemetry:
+ process.env.REDWOOD_DISABLE_TELEMETRY === undefined ||
+ process.env.REDWOOD_DISABLE_TELEMETRY === '',
+ },
+})
const tui = new RedwoodTUI()
@@ -67,7 +75,7 @@ async function executeCompatibilityCheck(templateDir) {
})
tui.startReactive(tuiContent)
- const [checksPassed, checksData] = await checkNodeAndYarnVersion(templateDir)
+ const [checksPassed, checksData] = await checkNodeVersion(templateDir)
if (checksPassed) {
tuiContent.update({
@@ -87,11 +95,6 @@ async function executeCompatibilityCheck(templateDir) {
semver.minVersion(checksData.node.wanted.raw)
)
- const foundYarnVersionIsLessThanRequired = semver.lt(
- checksData.yarn.version.version,
- semver.minVersion(checksData.yarn.wanted.raw)
- )
-
if (foundNodeVersionIsLessThanRequired) {
tui.stopReactive(true)
tui.displayError(
@@ -127,41 +130,6 @@ async function executeCompatibilityCheck(templateDir) {
process.exit(1)
}
- if (foundYarnVersionIsLessThanRequired) {
- tui.stopReactive(true)
- tui.displayError(
- 'Compatibility checks failed',
- [
- ` You need to upgrade the version of yarn you're using.`,
- ` You're using ${checksData.yarn.version.version} and we currently support node ${checksData.yarn.wanted.range}.`,
- '',
- ` Please use tools like corepack to change to a compatible version.`,
- ` See: ${terminalLink(
- 'How to - Using Yarn',
- 'https://redwoodjs.com/docs/how-to/using-yarn',
- {
- fallback: () =>
- 'How to - Using Yarn https://redwoodjs.com/docs/how-to/using-yarn',
- }
- )}`,
- ` See: ${terminalLink(
- 'Tutorial - Prerequisites',
- 'https://redwoodjs.com/docs/tutorial/chapter1/prerequisites',
- {
- fallback: () =>
- 'Tutorial - Prerequisites https://redwoodjs.com/docs/tutorial/chapter1/prerequisites',
- }
- )}`,
- '',
- ...USE_GITPOD_TEXT,
- ].join('\n')
- )
-
- recordErrorViaTelemetry('Compatibility checks failed')
- await shutdownTelemetry()
- process.exit(1)
- }
-
tui.stopReactive(true)
tui.displayWarning(
'Compatibility checks failed',
@@ -220,9 +188,9 @@ async function executeCompatibilityCheck(templateDir) {
* This type has to be updated if the engines field in the create redwood app template package.json is updated.
* @returns [boolean, Record<'node' | 'yarn', any>]
*/
-function checkNodeAndYarnVersion(templateDir) {
+function checkNodeVersion(templateDir) {
return new Promise((resolve) => {
- const { engines } = require(path.join(templateDir, 'package.json'))
+ const { engines } = fs.readJSONSync(path.join(templateDir, 'package.json'))
checkNodeVersionCb(engines, (_error, result) => {
return resolve([result.isSatisfied, result.versions])
@@ -671,57 +639,58 @@ async function handleYarnInstallPreference(yarnInstallFlag) {
* - TODO - Add a list of what this function does
*/
async function createRedwoodApp() {
- // Introductory message
- tui.drawText(
- [
- `${RedwoodStyling.redwood('-'.repeat(66))}`,
- `${' '.repeat(16)}π²β‘οΈ ${RedwoodStyling.header(
- 'Welcome to RedwoodJS!'
- )} β‘οΈπ²`,
- `${RedwoodStyling.redwood('-'.repeat(66))}`,
- ].join('\n')
- )
-
const cli = yargs(hideBin(process.argv))
.scriptName(name)
- .usage('Usage: $0 [option]')
- .example('$0 newapp')
- .option('typescript', {
- alias: 'ts',
+ .usage('Usage: $0 ')
+ .example('$0 my-redwood-app')
+ .version(version)
+ .option('yes', {
+ alias: 'y',
default: null,
type: 'boolean',
- describe: 'Generate a TypeScript project.',
+ describe: 'Skip prompts and use defaults',
})
.option('overwrite', {
default: false,
type: 'boolean',
describe: "Create even if target directory isn't empty",
})
- .option('telemetry', {
- default: true,
+ .option('typescript', {
+ alias: 'ts',
+ default: null,
type: 'boolean',
- describe:
- 'Enables sending telemetry events for this create command and all Redwood CLI commands https://telemetry.redwoodjs.com',
+ describe: 'Generate a TypeScript project',
})
.option('git-init', {
alias: 'git',
default: null,
type: 'boolean',
- describe: 'Initialize a git repository.',
+ describe: 'Initialize a git repository',
})
.option('commit-message', {
alias: 'm',
default: null,
type: 'string',
- describe: 'Commit message for the initial commit.',
+ describe: 'Commit message for the initial commit',
})
- .option('yes', {
- alias: 'y',
- default: null,
+ .option('telemetry', {
+ default: true,
type: 'boolean',
- describe: 'Skip prompts and use defaults.',
+ describe:
+ 'Enables sending telemetry events for this create command and all Redwood CLI commands https://telemetry.redwoodjs.com',
})
- .version(version)
+
+ const parsedFlags = cli.parse()
+
+ tui.drawText(
+ [
+ `${RedwoodStyling.redwood('-'.repeat(66))}`,
+ `${' '.repeat(16)}π²β‘οΈ ${RedwoodStyling.header(
+ 'Welcome to RedwoodJS!'
+ )} β‘οΈπ²`,
+ `${RedwoodStyling.redwood('-'.repeat(66))}`,
+ ].join('\n')
+ )
const _isYarnBerryOrNewer = isYarnBerryOrNewer()
@@ -734,8 +703,6 @@ async function createRedwoodApp() {
})
}
- const parsedFlags = cli.parse()
-
// Extract the args as provided by the user in the command line
// TODO: Make all flags have the 'flag' suffix
const args = parsedFlags._
@@ -743,7 +710,6 @@ async function createRedwoodApp() {
parsedFlags['yarn-install'] ?? !_isYarnBerryOrNewer ? parsedFlags.yes : null
const typescriptFlag = parsedFlags.typescript ?? parsedFlags.yes
const overwrite = parsedFlags.overwrite
- // telemetry, // Extracted above to check if telemetry is disabled before we even reach this point
const gitInitFlag = parsedFlags['git-init'] ?? parsedFlags.yes
const commitMessageFlag =
parsedFlags['commit-message'] ??
@@ -756,7 +722,7 @@ async function createRedwoodApp() {
// Get the directory for installation from the args
let targetDir = String(args).replace(/,/g, '-')
- const templatesDir = path.resolve(__dirname, '../templates')
+ const templatesDir = fileURLToPath(new URL('../templates', import.meta.url))
// Engine check
await executeCompatibilityCheck(path.join(templatesDir, 'ts'))
@@ -845,32 +811,29 @@ async function createRedwoodApp() {
)
}
-;(async () => {
- // Conditionally start telemetry
- if (telemetry !== 'false' && !process.env.REDWOOD_DISABLE_TELEMETRY) {
- try {
- await startTelemetry()
- } catch (error) {
- console.error('Telemetry startup error')
- console.error(error)
- }
- }
-
- // Execute create redwood app within a span
- const tracer = trace.getTracer('redwoodjs')
- await tracer.startActiveSpan('create-redwood-app', async (span) => {
- await createRedwoodApp()
-
- // Span housekeeping
- span?.setStatus({ code: SpanStatusCode.OK })
- span?.end()
- })
-
- // Shutdown telemetry, ensures data is sent before the process exits
+if (telemetry) {
try {
- await shutdownTelemetry()
+ await startTelemetry()
} catch (error) {
- console.error('Telemetry shutdown error')
+ console.error('Telemetry startup error')
console.error(error)
}
-})()
+}
+
+// Execute create redwood app within a span
+const tracer = trace.getTracer('redwoodjs')
+await tracer.startActiveSpan('create-redwood-app', async (span) => {
+ await createRedwoodApp()
+
+ // Span housekeeping
+ span?.setStatus({ code: SpanStatusCode.OK })
+ span?.end()
+})
+
+// Shutdown telemetry, ensures data is sent before the process exits
+try {
+ await shutdownTelemetry()
+} catch (error) {
+ console.error('Telemetry shutdown error')
+ console.error(error)
+}
diff --git a/packages/create-redwood-app/src/telemetry.js b/packages/create-redwood-app/src/telemetry.js
index 2f183e021840..471261101029 100644
--- a/packages/create-redwood-app/src/telemetry.js
+++ b/packages/create-redwood-app/src/telemetry.js
@@ -12,7 +12,7 @@ import envinfo from 'envinfo'
import system from 'systeminformation'
import { v4 as uuidv4 } from 'uuid'
-import { name as packageName, version as packageVersion } from '../package'
+import { name as packageName, version as packageVersion } from '../package.json'
/**
* @type NodeTracerProvider
diff --git a/packages/create-redwood-app/tests/e2e.test.mjs b/packages/create-redwood-app/tests/e2e.test.js
similarity index 74%
rename from packages/create-redwood-app/tests/e2e.test.mjs
rename to packages/create-redwood-app/tests/e2e.test.js
index 488f3d5178c2..7ddee4eace96 100644
--- a/packages/create-redwood-app/tests/e2e.test.mjs
+++ b/packages/create-redwood-app/tests/e2e.test.js
@@ -1,4 +1,3 @@
-#!/usr/bin/env node
/* eslint-env node */
import { cd, fs, $ } from 'zx'
@@ -7,36 +6,31 @@ const projectPath = await fs.realpath(process.env.PROJECT_PATH)
cd(projectPath)
-describe('crwa', () => {
+describe('create-redwood-app', () => {
test('--help', async () => {
const p = await $`yarn create-redwood-app --help`
expect(p.exitCode).toEqual(0)
expect(p.stdout).toMatchInlineSnapshot(`
- "------------------------------------------------------------------
- π²β‘οΈ Welcome to RedwoodJS! β‘οΈπ²
- ------------------------------------------------------------------
- Usage: create-redwood-app [option]
+ "Usage: create-redwood-app
Options:
--help Show help [boolean]
- --typescript, --ts Generate a TypeScript project.
- [boolean] [default: null]
+ --version Show version number [boolean]
+ -y, --yes Skip prompts and use defaults[boolean] [default: null]
--overwrite Create even if target directory isn't empty
[boolean] [default: false]
+ --typescript, --ts Generate a TypeScript project[boolean] [default: null]
+ --git-init, --git Initialize a git repository [boolean] [default: null]
+ -m, --commit-message Commit message for the initial commit
+ [string] [default: null]
--telemetry Enables sending telemetry events for this create
command and all Redwood CLI commands
https://telemetry.redwoodjs.com
[boolean] [default: true]
- --git-init, --git Initialize a git repository. [boolean] [default: null]
- -m, --commit-message Commit message for the initial commit.
- [string] [default: null]
- -y, --yes Skip prompts and use defaults.
- [boolean] [default: null]
- --version Show version number [boolean]
Examples:
- create-redwood-app newapp
+ create-redwood-app my-redwood-app
[?25l[?25h"
`)
expect(p.stderr).toMatchInlineSnapshot(`"[?25l[?25h"`)
@@ -47,10 +41,7 @@ describe('crwa', () => {
expect(p.exitCode).toEqual(0)
expect(p.stdout).toMatchInlineSnapshot(`
- "------------------------------------------------------------------
- π²β‘οΈ Welcome to RedwoodJS! β‘οΈπ²
- ------------------------------------------------------------------
- 6.0.7
+ "6.0.7
[?25l[?25h"
`)
expect(p.stderr).toMatchInlineSnapshot(`"[?25l[?25h"`)
@@ -59,10 +50,6 @@ describe('crwa', () => {
test('--yes, -y', async () => {
const p = await $`yarn create-redwood-app ./redwood-app --yes`
- // await $`yarn create-redwood-app redwood-app -y`
- // # `yarn pack` seems to ignore `.yarnrc.yml`
- // # cp "$SCRIPT_DIR/templates/ts/.yarnrc.yml" "$CRWA_ESM_TESTING_DIR"
-
expect(p.exitCode).toEqual(0)
expect(p.stdout).toMatchInlineSnapshot(`
"------------------------------------------------------------------
@@ -96,7 +83,7 @@ describe('crwa', () => {
await fs.rm('./redwood-app', { recursive: true, force: true })
})
- test.failing('fails on unknown options', async () => {
+ it.failing('fails on unknown options', async () => {
try {
await $`yarn create-redwood-app --unknown-options`.timeout(2500)
// Fail the test if the function didn't throw.
diff --git a/packages/create-redwood-app/tests/e2e_prompts.sh b/packages/create-redwood-app/tests/e2e_prompts.sh
index 3099ee4c5744..c017c7b184fc 100755
--- a/packages/create-redwood-app/tests/e2e_prompts.sh
+++ b/packages/create-redwood-app/tests/e2e_prompts.sh
@@ -17,11 +17,11 @@ expect "Where would you like to create your Redwood app?"
send "$projectDirectory\n"
expect "Select your preferred language"
-# TypeScript
+# β― TypeScript
send "\n"
expect "Do you want to initialize a git repo?"
-# Yes
+# β― Yes
send "\n"
expect "Enter a commit message"
diff --git a/packages/create-redwood-app/tests/e2e_prompts_git.sh b/packages/create-redwood-app/tests/e2e_prompts_git.sh
index e43f514a2bce..655896cc138c 100755
--- a/packages/create-redwood-app/tests/e2e_prompts_git.sh
+++ b/packages/create-redwood-app/tests/e2e_prompts_git.sh
@@ -17,7 +17,7 @@ expect "Where would you like to create your Redwood app?"
send "$projectDirectory\n"
expect "Select your preferred language"
-# TypeScript
+# β― TypeScript
send "\n"
expect "Enter a commit message"
diff --git a/packages/create-redwood-app/tests/e2e_prompts_m.sh b/packages/create-redwood-app/tests/e2e_prompts_m.sh
index c9c24613e0b1..30d5e61e6ce3 100755
--- a/packages/create-redwood-app/tests/e2e_prompts_m.sh
+++ b/packages/create-redwood-app/tests/e2e_prompts_m.sh
@@ -17,11 +17,11 @@ expect "Where would you like to create your Redwood app?"
send "$projectDirectory\n"
expect "Select your preferred language"
-# TypeScript
+# β― TypeScript
send "\n"
expect "Do you want to initialize a git repo?"
-# Yes
+# β― Yes
send "\n"
expect eof
diff --git a/packages/create-redwood-app/tests/e2e_prompts_node_greater.sh b/packages/create-redwood-app/tests/e2e_prompts_node_greater.sh
new file mode 100755
index 000000000000..73a737735458
--- /dev/null
+++ b/packages/create-redwood-app/tests/e2e_prompts_node_greater.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/expect
+
+# You have to set your Node version to 21+ before running this test.
+
+set projectPath $env(PROJECT_PATH)
+
+if {$projectPath eq ""} {
+ puts "PROJECT_PATH is not set"
+ exit
+}
+
+cd $projectPath
+
+set projectDirectory "redwood-app-prompt-node-greater-test"
+
+spawn yarn create-redwood-app
+
+expect "How would you like to proceed?"
+# β― Override error and continue install
+send "\n"
+
+expect "Where would you like to create your Redwood app?"
+send "$projectDirectory\n"
+
+expect "Select your preferred language"
+# β― TypeScript
+send "\n"
+
+expect "Do you want to initialize a git repo?"
+# β― Yes
+send "\n"
+
+expect "Enter a commit message"
+send "first\n"
+
+expect eof
+catch wait result
+set exitStatus [lindex $result 3]
+
+if {$exitStatus == 0} {
+ puts "Success"
+ exec rm -rf $projectDirectory
+ exit 0
+} else {
+ puts "Error: The process failed with exit status $exitStatus"
+ exec rm -rf $projectDirectory
+ exit 1
+}
diff --git a/packages/create-redwood-app/tests/e2e_prompts_node_less.sh b/packages/create-redwood-app/tests/e2e_prompts_node_less.sh
new file mode 100755
index 000000000000..d8187de802fe
--- /dev/null
+++ b/packages/create-redwood-app/tests/e2e_prompts_node_less.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/expect
+
+# You have to set your Node version to 18 before running this one.
+
+set projectPath $env(PROJECT_PATH)
+
+if {$projectPath eq ""} {
+ puts "PROJECT_PATH is not set"
+ exit
+}
+
+cd $projectPath
+
+set projectDirectory "redwood-app-prompt-node-less-test"
+
+spawn yarn create-redwood-app
+
+expect eof
+catch wait result
+set exitStatus [lindex $result 3]
+
+if {$exitStatus == 1} {
+ puts "Success"
+ exec rm -rf $projectDirectory
+ exit 0
+} else {
+ puts "Error: The process didn't fail with exit status $exitStatus"
+ exec rm -rf $projectDirectory
+ exit 1
+}
diff --git a/packages/create-redwood-app/tests/e2e_prompts_overwrite.sh b/packages/create-redwood-app/tests/e2e_prompts_overwrite.sh
index ad8d890f0100..dd828aa1a7bf 100755
--- a/packages/create-redwood-app/tests/e2e_prompts_overwrite.sh
+++ b/packages/create-redwood-app/tests/e2e_prompts_overwrite.sh
@@ -20,18 +20,18 @@ expect "Where would you like to create your Redwood app?"
send "$projectDirectory\n"
expect "Select your preferred language"
-# TypeScript
+# β― TypeScript
send "\n"
expect "Do you want to initialize a git repo?"
-# Yes
+# β― Yes
send "\n"
expect "Enter a commit message"
send "first\n"
expect "How would you like to proceed?"
-# Quit install
+# β― Quit install
send "\n"
expect eof
diff --git a/packages/create-redwood-app/tests/e2e_prompts_ts.sh b/packages/create-redwood-app/tests/e2e_prompts_ts.sh
index 8a8f17c52847..7a32631c8c8a 100755
--- a/packages/create-redwood-app/tests/e2e_prompts_ts.sh
+++ b/packages/create-redwood-app/tests/e2e_prompts_ts.sh
@@ -17,7 +17,7 @@ expect "Where would you like to create your Redwood app?"
send "$projectDirectory\n"
expect "Do you want to initialize a git repo?"
-# Yes
+# β― Yes
send "\n"
expect "Enter a commit message"
diff --git a/packages/create-redwood-app/tests/templates.test.mjs b/packages/create-redwood-app/tests/templates.test.js
similarity index 99%
rename from packages/create-redwood-app/tests/templates.test.mjs
rename to packages/create-redwood-app/tests/templates.test.js
index a1bbfb76e13e..f8ef6ddf4f6c 100644
--- a/packages/create-redwood-app/tests/templates.test.mjs
+++ b/packages/create-redwood-app/tests/templates.test.js
@@ -1,5 +1,5 @@
+import path from 'node:path'
import { fileURLToPath } from 'node:url'
-import path from 'path'
import klawSync from 'klaw-sync'
@@ -7,7 +7,7 @@ const TS_TEMPLATE_DIR = fileURLToPath(
new URL('../templates/ts', import.meta.url)
)
-describe('template', () => {
+describe('TS template', () => {
it('files should not have changed unintentionally', () => {
expect(getDirectoryStructure(TS_TEMPLATE_DIR)).toMatchInlineSnapshot(`
[
From 5210e06938568a584805a398678814d417a9a002 Mon Sep 17 00:00:00 2001
From: Dominic Saadi
Date: Sun, 7 Jan 2024 06:30:55 -0800
Subject: [PATCH 17/18] fix(server): error early on incompatible config
(apiHost and apiUrl) (#9808)
I was working on Redwood's server config with @Josh-Walker-GM. One of
the more unfortunate footguns is when you're serving the web side alone
while the api side is somewhere else, on a completely different host. If
you don't pass `apiHost`, and `apiUrl` isn't fully qualified, what
happens is that the web side requests itself, resulting in network
responses like this:
An easy fix that would go a long way is just erroring out if we detect
the config is incompatible and not starting the server at all:
```
# In redwood.toml, apiUrl is `/.redwood/functions`, a relative URL, and apiHost isn't passed the at the CLI:
% yarn rw serve web
Error: If you don't provide apiHost, apiUrl needs to be a fully-qualified URL. But apiUrl is /.redwood/functions.
```
We used a specific exit code because `yarn rw serve web` is a bin that's
calling a bin `yarn rw-server-web`. We plan to fix that by deduping code
across packages, so this specific exit code is just a stopgap till we
get that work done. The reason we're doing this fix first is that again,
it's easy and would solve a big problem.
We referenced this doc on exit code conventions which stated that 64-113
was up for grabs: https://tldp.org/LDP/abs/html/exitcodes.html.
---
packages/cli/src/commands/serveWebHandler.js | 47 +++++++++++++-------
packages/web-server/src/server.ts | 28 +++++++++++-
2 files changed, 58 insertions(+), 17 deletions(-)
diff --git a/packages/cli/src/commands/serveWebHandler.js b/packages/cli/src/commands/serveWebHandler.js
index ae3c33972132..28fb9af714c8 100644
--- a/packages/cli/src/commands/serveWebHandler.js
+++ b/packages/cli/src/commands/serveWebHandler.js
@@ -2,6 +2,8 @@ import execa from 'execa'
import { getPaths } from '@redwoodjs/project-config'
+import { exitWithError } from '../lib/exit'
+
export const webSsrServerHandler = async () => {
await execa('yarn', ['rw-serve-fe'], {
cwd: getPaths().web.base,
@@ -11,21 +13,34 @@ export const webSsrServerHandler = async () => {
}
export const webServerHandler = async (argv) => {
- await execa(
- 'yarn',
- [
- 'rw-web-server',
- '--port',
- argv.port,
- '--socket',
- argv.socket,
- '--api-host',
- argv.apiHost,
- ],
- {
- cwd: getPaths().base,
- stdio: 'inherit',
- shell: true,
+ try {
+ await execa(
+ 'yarn',
+ [
+ 'rw-web-server',
+ '--port',
+ argv.port,
+ '--socket',
+ argv.socket,
+ '--api-host',
+ argv.apiHost,
+ ],
+ {
+ cwd: getPaths().base,
+ stdio: 'inherit',
+ shell: true,
+ }
+ )
+ } catch (e) {
+ // `@redwoodjs/web-server` uses a custom error exit code to tell this handler that an error has already been handled.
+ // While any other exit code than `0` is considered an error, there seems to be some conventions around some of them
+ // like `127`, etc. We chose 64 because it's in the range where there deliberately aren't any previous conventions.
+ // See https://tldp.org/LDP/abs/html/exitcodes.html.
+ if (e.exitCode === 64) {
+ process.exitCode = 1
+ return
}
- )
+
+ exitWithError(e)
+ }
}
diff --git a/packages/web-server/src/server.ts b/packages/web-server/src/server.ts
index d6a09b2eccc4..dcc5ea955405 100644
--- a/packages/web-server/src/server.ts
+++ b/packages/web-server/src/server.ts
@@ -18,7 +18,15 @@ interface Opts {
apiHost?: string
}
-// no help option...
+function isFullyQualifiedUrl(url: string) {
+ try {
+ // eslint-disable-next-line no-new
+ new URL(url)
+ return true
+ } catch (e) {
+ return false
+ }
+}
async function serve() {
// Parse server file args
@@ -39,6 +47,24 @@ async function serve() {
const port = options.port ? parseInt(options.port) : redwoodConfig.web.port
const apiUrl = redwoodConfig.web.apiUrl
+ if (!options.apiHost && !isFullyQualifiedUrl(apiUrl)) {
+ console.error(
+ `${chalk.red('Error')}: If you don't provide ${chalk.magenta(
+ 'apiHost'
+ )}, ${chalk.magenta(
+ 'apiUrl'
+ )} needs to be a fully-qualified URL. But ${chalk.magenta(
+ 'apiUrl'
+ )} is ${chalk.yellow(apiUrl)}.`
+ )
+ // We're using a custom error exit code here to tell `@redwoodjs/cli` that this error has been handled.
+ // While any other exit code than `0` is considered an error, there seems to be some conventions around some of them
+ // like `127`, etc. We chose 64 because it's in the range where there deliberately aren't any previous conventions.
+ // See https://tldp.org/LDP/abs/html/exitcodes.html.
+ process.exitCode = 64
+ return
+ }
+
const tsServer = Date.now()
// Load .env files
From 80e4a4f56909f525eb6735aa93de4ecf42db3089 Mon Sep 17 00:00:00 2001
From: David Thyresson
Date: Mon, 8 Jan 2024 05:08:55 -0500
Subject: [PATCH 18/18] chore: Improved Possible Types DX and Framework
integration for GraphQL Fragments with Union and Interface support (#9594)
Co-authored-by: Tobbe Lundberg
---
.github/workflows/ci.yml | 90 ++++
.../{web => }/.editorconfig | 0
.../fragment-test-project/.env.defaults | 19 +
.../fragment-test-project/.env.example | 4 +
__fixtures__/fragment-test-project/.gitignore | 22 +
.../fragment-test-project/.redwood/README.md | 44 ++
.../.redwood/schema.graphql | 102 ----
.../.vscode/extensions.json | 16 +
.../fragment-test-project/.vscode/launch.json | 56 ++
.../.vscode/settings.json | 11 +
.../fragment-test-project/.vscode/tasks.json | 29 +
.../fragment-test-project/.yarnrc.yml | 15 +
__fixtures__/fragment-test-project/README.md | 124 ++++-
.../migration.sql | 34 ++
.../migration.sql | 8 +
.../migration.sql | 28 +
.../api/db/migrations/migration_lock.toml | 3 +
.../api/db/schema.prisma | 59 ++-
.../fragment-test-project/api/package.json | 5 +-
.../fragment-test-project/api/src/.keep | 0
.../api/src/__tests__/context.test.ts | 14 +
.../requireAuth/requireAuth.test.ts | 4 +-
.../api/src/functions/auth.ts | 202 +++++++
.../api/src/functions/graphql.ts | 6 +
.../api/src/graphql/contacts.sdl.ts | 32 ++
.../api/src/graphql/posts.sdl.ts | 33 ++
.../api/src/graphql/produces.sdl.ts | 54 ++
.../api/src/graphql/stalls.sdl.ts | 27 +-
.../api/src/graphql/users.sdl.ts | 25 +
.../fragment-test-project/api/src/lib/auth.ts | 128 ++++-
.../fragment-test-project/api/src/lib/db.ts | 4 +-
.../api/src/lib/persistedOperations.json | 4 -
.../services/contacts/contacts.scenarios.ts | 12 +
.../src/services/contacts/contacts.test.ts | 59 +++
.../api/src/services/contacts/contacts.ts | 37 ++
.../api/src/services/fruit.ts | 29 -
.../api/src/services/groceries.ts | 6 +-
.../api/src/services/posts/posts.scenarios.ts | 38 ++
.../api/src/services/posts/posts.test.ts | 55 ++
.../api/src/services/posts/posts.ts | 42 ++
.../services/produces/produces.scenarios.ts | 27 +
.../src/services/produces/produces.test.ts | 67 +++
.../api/src/services/produces/produces.ts | 47 ++
.../api/src/services/stalls.ts | 21 -
.../src/services/stalls/stalls.scenarios.ts | 11 +
.../api/src/services/stalls/stalls.test.ts | 50 ++
.../api/src/services/stalls/stalls.ts | 45 ++
.../api/src/services/users/users.scenarios.ts | 26 +
.../api/src/services/users/users.test.ts | 10 +
.../api/src/services/users/users.ts | 17 +
.../api/src/services/vegetables.ts | 22 -
.../fragment-test-project/jest.config.js | 8 +
.../fragment-test-project/package.json | 23 +
.../fragment-test-project/prettier.config.js | 20 +
.../fragment-test-project/redwood.toml | 18 +-
.../fragment-test-project/scripts/seed.ts | 195 +++++++
.../scripts/tsconfig.json | 42 ++
.../web/config/postcss.config.js | 8 +
.../web/config/tailwind.config.js | 8 +
.../fragment-test-project/web/package.json | 22 +-
.../web/public/README.md | 35 ++
.../web/public/favicon.png | Bin 0 -> 1741 bytes
.../web/public/robots.txt | 2 +
.../fragment-test-project/web/src/App.tsx | 20 +-
.../web/src/Redwood.stories.mdx | 15 +
.../fragment-test-project/web/src/Routes.tsx | 41 +-
.../fragment-test-project/web/src/auth.ts | 5 +
.../src/components/Author/Author.stories.tsx | 35 ++
.../web/src/components/Author/Author.test.tsx | 19 +
.../web/src/components/Author/Author.tsx | 16 +
.../components/AuthorCell/AuthorCell.mock.ts | 8 +
.../AuthorCell/AuthorCell.stories.tsx | 35 ++
.../components/AuthorCell/AuthorCell.test.tsx | 42 ++
.../src/components/AuthorCell/AuthorCell.tsx | 39 ++
.../components/BlogPost/BlogPost.stories.tsx | 26 +
.../src/components/BlogPost/BlogPost.test.tsx | 14 +
.../web/src/components/BlogPost/BlogPost.tsx | 41 ++
.../BlogPostCell/BlogPostCell.mock.ts | 15 +
.../BlogPostCell/BlogPostCell.stories.tsx | 35 ++
.../BlogPostCell/BlogPostCell.test.tsx | 42 ++
.../components/BlogPostCell/BlogPostCell.tsx | 46 ++
.../BlogPostsCell/BlogPostsCell.mock.ts | 41 ++
.../BlogPostsCell/BlogPostsCell.stories.tsx | 35 ++
.../BlogPostsCell/BlogPostsCell.test.tsx | 42 ++
.../BlogPostsCell/BlogPostsCell.tsx | 43 ++
.../web/src/components/{Card => }/Card.tsx | 0
.../components/Contact/Contact/Contact.tsx | 98 ++++
.../Contact/ContactCell/ContactCell.tsx | 40 ++
.../Contact/ContactForm/ContactForm.tsx | 101 ++++
.../components/Contact/Contacts/Contacts.tsx | 102 ++++
.../Contact/ContactsCell/ContactsCell.tsx | 48 ++
.../EditContactCell/EditContactCell.tsx | 89 ++++
.../Contact/NewContact/NewContact.tsx | 55 ++
.../components/{Fruit.tsx => FruitInfo.tsx} | 12 +-
.../Post/EditPostCell/EditPostCell.tsx | 78 +++
.../src/components/Post/NewPost/NewPost.tsx | 52 ++
.../web/src/components/Post/Post/Post.tsx | 98 ++++
.../src/components/Post/PostCell/PostCell.tsx | 38 ++
.../src/components/Post/PostForm/PostForm.tsx | 102 ++++
.../web/src/components/Post/Posts/Posts.tsx | 102 ++++
.../components/Post/PostsCell/PostsCell.tsx | 45 ++
.../{Produce.tsx => ProduceInfo.tsx} | 8 +-
.../components/{Stall.tsx => StallInfo.tsx} | 6 +-
.../{Vegetable.tsx => VegetableInfo.tsx} | 12 +-
.../WaterfallBlogPostCell.mock.ts | 15 +
.../WaterfallBlogPostCell.stories.tsx | 35 ++
.../WaterfallBlogPostCell.test.tsx | 42 ++
.../WaterfallBlogPostCell.tsx | 67 +++
.../web/src/entry.client.tsx | 6 +
.../web/src/graphql/persistedOperations.json | 4 -
.../web/src/graphql/possible-types.ts | 12 -
.../web/src/graphql/possibleTypes.ts | 30 +-
.../fragment-test-project/web/src/index.css | 13 +
.../layouts/BlogLayout/BlogLayout.stories.tsx | 13 +
.../layouts/BlogLayout/BlogLayout.test.tsx | 14 +
.../web/src/layouts/BlogLayout/BlogLayout.tsx | 80 +++
.../layouts/ScaffoldLayout/ScaffoldLayout.tsx | 37 ++
.../web/src/lib/formatters.test.tsx | 192 +++++++
.../web/src/lib/formatters.tsx | 58 ++
.../src/pages/AboutPage/AboutPage.stories.tsx | 13 +
.../src/pages/AboutPage/AboutPage.test.tsx | 14 +
.../web/src/pages/AboutPage/AboutPage.tsx | 13 +
.../BlogPostPage/BlogPostPage.routeHooks.ts | 5 +
.../BlogPostPage/BlogPostPage.stories.tsx | 17 +
.../pages/BlogPostPage/BlogPostPage.test.tsx | 14 +
.../src/pages/BlogPostPage/BlogPostPage.tsx | 20 +
.../pages/Contact/ContactPage/ContactPage.tsx | 11 +
.../Contact/ContactsPage/ContactsPage.tsx | 7 +
.../EditContactPage/EditContactPage.tsx | 11 +
.../Contact/NewContactPage/NewContactPage.tsx | 7 +
.../ContactUsPage/ContactUsPage.stories.tsx | 13 +
.../ContactUsPage/ContactUsPage.test.tsx | 14 +
.../src/pages/ContactUsPage/ContactUsPage.tsx | 108 ++++
.../pages/DoublePage/DoublePage.stories.tsx | 13 +
.../src/pages/DoublePage/DoublePage.test.tsx | 14 +
.../web/src/pages/DoublePage/DoublePage.tsx | 25 +
.../ForgotPasswordPage/ForgotPasswordPage.tsx | 94 ++++
.../GroceriesPage/GroceriesPage.stories.tsx | 13 +
.../GroceriesPage/GroceriesPage.test.tsx | 14 +
.../src/pages/GroceriesPage/GroceriesPage.tsx | 32 +-
.../src/pages/HomePage/HomePage.stories.tsx | 13 +
.../web/src/pages/HomePage/HomePage.test.tsx | 14 +
.../web/src/pages/HomePage/HomePage.tsx | 10 +
.../web/src/pages/LoginPage/LoginPage.tsx | 134 +++++
.../pages/Post/EditPostPage/EditPostPage.tsx | 11 +
.../pages/Post/NewPostPage/NewPostPage.tsx | 7 +
.../web/src/pages/Post/PostPage/PostPage.tsx | 11 +
.../src/pages/Post/PostsPage/PostsPage.tsx | 7 +
.../pages/ProfilePage/ProfilePage.stories.tsx | 13 +
.../pages/ProfilePage/ProfilePage.test.tsx | 21 +
.../web/src/pages/ProfilePage/ProfilePage.tsx | 55 ++
.../ResetPasswordPage/ResetPasswordPage.tsx | 121 +++++
.../web/src/pages/SignupPage/SignupPage.tsx | 148 ++++++
.../WaterfallPage/WaterfallPage.routeHooks.ts | 3 +
.../WaterfallPage/WaterfallPage.stories.tsx | 17 +
.../WaterfallPage/WaterfallPage.test.tsx | 14 +
.../src/pages/WaterfallPage/WaterfallPage.tsx | 11 +
.../web/src/scaffold.css | 243 +++++++++
.../fragment-test-project/web/tsconfig.json | 5 +-
.../web/types/graphql.d.ts | 353 ++++++++++++-
.../web/types/possible-types.ts | 20 -
.../fragment-test-project/web/vite.config.ts | 27 +-
__fixtures__/test-project/scripts/seed.ts | 26 +-
package.json | 1 +
packages/cli/src/commands/buildHandler.js | 19 +
.../templates/js/web/src/App.jsx | 9 +-
.../js/web/src/graphql/possibleTypes.js | 5 -
.../templates/ts/web/src/App.tsx | 9 +-
.../ts/web/src/graphql/possibleTypes.ts | 11 -
.../tests/templates.test.js | 4 -
.../internal/src/generate/clientPreset.ts | 1 +
packages/vite/src/index.ts | 1 -
packages/web/src/apollo/index.tsx | 2 +
.../web/src/components/cell/createCell.tsx | 4 +-
.../fragments-dev/playwright.config.ts | 25 +
.../fragments-dev/tests/fragments.spec.ts | 17 +
.../fragments-serve/playwright.config.ts | 21 +
.../fragments-serve/tests/fragments.spec.ts | 17 +
tasks/test-project/add-gql-fragments.ts | 29 +
tasks/test-project/codemods/groceriesPage.ts | 208 ++++++++
tasks/test-project/codemods/models.js | 28 +-
tasks/test-project/codemods/producesSdl.ts | 5 +
tasks/test-project/codemods/seed.js | 26 +-
tasks/test-project/codemods/seedFragments.ts | 83 +++
.../rebuild-fragments-test-project-fixture.ts | 495 ++++++++++++++++++
.../rebuild-test-project-fixture.ts | 2 +-
tasks/test-project/tasks.js | 122 ++++-
.../templates/api/groceries.sdl.ts | 49 ++
tasks/test-project/templates/api/groceries.ts | 32 ++
tasks/test-project/templates/web/Card.tsx | 9 +
.../test-project/templates/web/FruitInfo.tsx | 37 ++
.../templates/web/ProduceInfo.tsx | 28 +
.../test-project/templates/web/StallInfo.tsx | 26 +
.../templates/web/VegetableInfo.tsx | 37 ++
tasks/test-project/tui-tasks.js | 118 +++++
195 files changed, 7373 insertions(+), 426 deletions(-)
rename __fixtures__/fragment-test-project/{web => }/.editorconfig (100%)
create mode 100644 __fixtures__/fragment-test-project/.env.defaults
create mode 100644 __fixtures__/fragment-test-project/.env.example
create mode 100644 __fixtures__/fragment-test-project/.gitignore
create mode 100644 __fixtures__/fragment-test-project/.redwood/README.md
delete mode 100644 __fixtures__/fragment-test-project/.redwood/schema.graphql
create mode 100644 __fixtures__/fragment-test-project/.vscode/extensions.json
create mode 100644 __fixtures__/fragment-test-project/.vscode/launch.json
create mode 100644 __fixtures__/fragment-test-project/.vscode/settings.json
create mode 100644 __fixtures__/fragment-test-project/.vscode/tasks.json
create mode 100644 __fixtures__/fragment-test-project/.yarnrc.yml
create mode 100644 __fixtures__/fragment-test-project/api/db/migrations/20220101120000_create_post_user/migration.sql
create mode 100644 __fixtures__/fragment-test-project/api/db/migrations/20220102120000_create_contact/migration.sql
create mode 100644 __fixtures__/fragment-test-project/api/db/migrations/20240106111257_create_produce_stall/migration.sql
create mode 100644 __fixtures__/fragment-test-project/api/db/migrations/migration_lock.toml
delete mode 100644 __fixtures__/fragment-test-project/api/src/.keep
create mode 100644 __fixtures__/fragment-test-project/api/src/__tests__/context.test.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/functions/auth.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/graphql/contacts.sdl.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/graphql/posts.sdl.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/graphql/produces.sdl.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/graphql/users.sdl.ts
delete mode 100644 __fixtures__/fragment-test-project/api/src/lib/persistedOperations.json
create mode 100644 __fixtures__/fragment-test-project/api/src/services/contacts/contacts.scenarios.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/services/contacts/contacts.test.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/services/contacts/contacts.ts
delete mode 100644 __fixtures__/fragment-test-project/api/src/services/fruit.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/services/posts/posts.scenarios.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/services/posts/posts.test.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/services/posts/posts.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/services/produces/produces.scenarios.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/services/produces/produces.test.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/services/produces/produces.ts
delete mode 100644 __fixtures__/fragment-test-project/api/src/services/stalls.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/services/stalls/stalls.scenarios.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/services/stalls/stalls.test.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/services/stalls/stalls.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/services/users/users.scenarios.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/services/users/users.test.ts
create mode 100644 __fixtures__/fragment-test-project/api/src/services/users/users.ts
delete mode 100644 __fixtures__/fragment-test-project/api/src/services/vegetables.ts
create mode 100644 __fixtures__/fragment-test-project/jest.config.js
create mode 100644 __fixtures__/fragment-test-project/package.json
create mode 100644 __fixtures__/fragment-test-project/prettier.config.js
create mode 100644 __fixtures__/fragment-test-project/scripts/seed.ts
create mode 100644 __fixtures__/fragment-test-project/scripts/tsconfig.json
create mode 100644 __fixtures__/fragment-test-project/web/config/postcss.config.js
create mode 100644 __fixtures__/fragment-test-project/web/config/tailwind.config.js
create mode 100644 __fixtures__/fragment-test-project/web/public/README.md
create mode 100644 __fixtures__/fragment-test-project/web/public/favicon.png
create mode 100644 __fixtures__/fragment-test-project/web/public/robots.txt
create mode 100644 __fixtures__/fragment-test-project/web/src/Redwood.stories.mdx
create mode 100644 __fixtures__/fragment-test-project/web/src/auth.ts
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Author/Author.stories.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Author/Author.test.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Author/Author.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/AuthorCell/AuthorCell.mock.ts
create mode 100644 __fixtures__/fragment-test-project/web/src/components/AuthorCell/AuthorCell.stories.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/AuthorCell/AuthorCell.test.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/AuthorCell/AuthorCell.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/BlogPost/BlogPost.stories.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/BlogPost/BlogPost.test.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/BlogPost/BlogPost.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.mock.ts
create mode 100644 __fixtures__/fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.stories.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.test.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.mock.ts
create mode 100644 __fixtures__/fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.stories.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.test.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.tsx
rename __fixtures__/fragment-test-project/web/src/components/{Card => }/Card.tsx (100%)
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Contact/Contact/Contact.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Contact/ContactCell/ContactCell.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Contact/ContactForm/ContactForm.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Contact/Contacts/Contacts.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Contact/ContactsCell/ContactsCell.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Contact/EditContactCell/EditContactCell.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Contact/NewContact/NewContact.tsx
rename __fixtures__/fragment-test-project/web/src/components/{Fruit.tsx => FruitInfo.tsx} (74%)
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Post/EditPostCell/EditPostCell.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Post/NewPost/NewPost.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Post/Post/Post.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Post/PostCell/PostCell.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Post/PostForm/PostForm.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Post/Posts/Posts.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/Post/PostsCell/PostsCell.tsx
rename __fixtures__/fragment-test-project/web/src/components/{Produce.tsx => ProduceInfo.tsx} (73%)
rename __fixtures__/fragment-test-project/web/src/components/{Stall.tsx => StallInfo.tsx} (83%)
rename __fixtures__/fragment-test-project/web/src/components/{Vegetable.tsx => VegetableInfo.tsx} (74%)
create mode 100644 __fixtures__/fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.mock.ts
create mode 100644 __fixtures__/fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.stories.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.test.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.tsx
delete mode 100644 __fixtures__/fragment-test-project/web/src/graphql/persistedOperations.json
delete mode 100644 __fixtures__/fragment-test-project/web/src/graphql/possible-types.ts
create mode 100644 __fixtures__/fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.stories.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.test.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/lib/formatters.test.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/lib/formatters.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/AboutPage/AboutPage.stories.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/AboutPage/AboutPage.test.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/AboutPage/AboutPage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.routeHooks.ts
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.stories.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.test.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/Contact/ContactPage/ContactPage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/Contact/ContactsPage/ContactsPage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/Contact/EditContactPage/EditContactPage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/Contact/NewContactPage/NewContactPage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.stories.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.test.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/DoublePage/DoublePage.stories.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/DoublePage/DoublePage.test.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/DoublePage/DoublePage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/GroceriesPage/GroceriesPage.stories.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/GroceriesPage/GroceriesPage.test.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/HomePage/HomePage.stories.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/HomePage/HomePage.test.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/HomePage/HomePage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/LoginPage/LoginPage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/Post/EditPostPage/EditPostPage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/Post/NewPostPage/NewPostPage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/Post/PostPage/PostPage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/Post/PostsPage/PostsPage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/ProfilePage/ProfilePage.stories.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/ProfilePage/ProfilePage.test.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/ProfilePage/ProfilePage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/SignupPage/SignupPage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.routeHooks.ts
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.stories.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.test.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.tsx
create mode 100644 __fixtures__/fragment-test-project/web/src/scaffold.css
delete mode 100644 __fixtures__/fragment-test-project/web/types/possible-types.ts
delete mode 100644 packages/create-redwood-app/templates/js/web/src/graphql/possibleTypes.js
delete mode 100644 packages/create-redwood-app/templates/ts/web/src/graphql/possibleTypes.ts
create mode 100644 tasks/smoke-tests/fragments-dev/playwright.config.ts
create mode 100644 tasks/smoke-tests/fragments-dev/tests/fragments.spec.ts
create mode 100644 tasks/smoke-tests/fragments-serve/playwright.config.ts
create mode 100644 tasks/smoke-tests/fragments-serve/tests/fragments.spec.ts
create mode 100755 tasks/test-project/add-gql-fragments.ts
create mode 100644 tasks/test-project/codemods/groceriesPage.ts
create mode 100644 tasks/test-project/codemods/producesSdl.ts
create mode 100644 tasks/test-project/codemods/seedFragments.ts
create mode 100755 tasks/test-project/rebuild-fragments-test-project-fixture.ts
create mode 100644 tasks/test-project/templates/api/groceries.sdl.ts
create mode 100644 tasks/test-project/templates/api/groceries.ts
create mode 100644 tasks/test-project/templates/web/Card.tsx
create mode 100644 tasks/test-project/templates/web/FruitInfo.tsx
create mode 100644 tasks/test-project/templates/web/ProduceInfo.tsx
create mode 100644 tasks/test-project/templates/web/StallInfo.tsx
create mode 100644 tasks/test-project/templates/web/VegetableInfo.tsx
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 923978293a20..8d439012c71e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -702,6 +702,96 @@ jobs:
steps:
- run: echo "Skipped"
+ fragments-smoke-tests:
+ needs: check
+
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+
+ name: π Fragments Smoke tests / ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+
+ env:
+ REDWOOD_CI: 1
+ REDWOOD_VERBOSE_TELEMETRY: 1
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Enable Corepack
+ run: corepack enable
+
+ - name: β¬’ Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+
+ - name: Enable Corepack
+ run: corepack enable
+
+ - name: π Set up yarn cache
+ uses: ./.github/actions/set-up-yarn-cache
+
+ - name: π Yarn install
+ run: yarn install --inline-builds
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+
+ - name: π¨ Build
+ run: yarn build
+
+ - name: π² Set up test project
+ id: set-up-test-project
+ uses: ./.github/actions/set-up-test-project
+ with:
+ bundler: vite
+ canary: true
+ env:
+ REDWOOD_DISABLE_TELEMETRY: 1
+ YARN_ENABLE_IMMUTABLE_INSTALLS: false
+
+ - name: Run Fragments codemods on test project
+ run: npx -y tsx ./tasks/test-project/add-gql-fragments ${{ steps.set-up-test-project.outputs.test-project-path }}
+ env:
+ REDWOOD_DISABLE_TELEMETRY: 1
+
+ - name: π Install playwright dependencies
+ run: npx playwright install --with-deps chromium
+
+ - name: Run Fragments dev smoke tests
+ working-directory: ./tasks/smoke-tests/fragments-dev
+ run: npx playwright test
+ env:
+ REDWOOD_TEST_PROJECT_PATH: '${{ steps.set-up-test-project.outputs.test-project-path }}'
+ REDWOOD_DISABLE_TELEMETRY: 1
+
+ - name: Build for production
+ working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }}
+ run: yarn rw build
+ env:
+ REDWOOD_DISABLE_TELEMETRY: 1
+
+ - name: Run Fragments serve smoke tests
+ working-directory: ./tasks/smoke-tests/fragments-serve
+ run: npx playwright test
+ env:
+ REDWOOD_TEST_PROJECT_PATH: '${{ steps.set-up-test-project.outputs.test-project-path }}'
+ REDWOOD_DISABLE_TELEMETRY: 1
+
+ fragments-smoke-tests-skip:
+ needs: detect-changes
+
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+
+ name: π Fragments Smoke tests / ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - run: echo "Skipped"
+
crwa:
needs: check
diff --git a/__fixtures__/fragment-test-project/web/.editorconfig b/__fixtures__/fragment-test-project/.editorconfig
similarity index 100%
rename from __fixtures__/fragment-test-project/web/.editorconfig
rename to __fixtures__/fragment-test-project/.editorconfig
diff --git a/__fixtures__/fragment-test-project/.env.defaults b/__fixtures__/fragment-test-project/.env.defaults
new file mode 100644
index 000000000000..fb88fb33b334
--- /dev/null
+++ b/__fixtures__/fragment-test-project/.env.defaults
@@ -0,0 +1,19 @@
+# These environment variables will be used by default if you do not create any
+# yourself in .env. This file should be safe to check into your version control
+# system. Any custom values should go in .env and .env should *not* be checked
+# into version control.
+
+# schema.prisma defaults
+DATABASE_URL=file:./dev.db
+
+# location of the test database for api service scenarios (defaults to ./.redwood/test.db if not set)
+# TEST_DATABASE_URL=file:./.redwood/test.db
+
+# disables Prisma CLI update notifier
+PRISMA_HIDE_UPDATE_MESSAGE=true
+
+# Option to override the current environment's default api-side log level
+# See: https://redwoodjs.com/docs/logger for level options, defaults to "trace" otherwise.
+# Most applications want "debug" or "info" during dev, "trace" when you have issues and "warn" in production.
+# Ordered by how verbose they are: trace | debug | info | warn | error | silent
+# LOG_LEVEL=debug
diff --git a/__fixtures__/fragment-test-project/.env.example b/__fixtures__/fragment-test-project/.env.example
new file mode 100644
index 000000000000..2a2de6c026ca
--- /dev/null
+++ b/__fixtures__/fragment-test-project/.env.example
@@ -0,0 +1,4 @@
+# DATABASE_URL=file:./dev.db
+# TEST_DATABASE_URL=file:./.redwood/test.db
+# PRISMA_HIDE_UPDATE_MESSAGE=true
+# LOG_LEVEL=trace
diff --git a/__fixtures__/fragment-test-project/.gitignore b/__fixtures__/fragment-test-project/.gitignore
new file mode 100644
index 000000000000..9b8149560d9b
--- /dev/null
+++ b/__fixtures__/fragment-test-project/.gitignore
@@ -0,0 +1,22 @@
+.idea
+.DS_Store
+.env
+.netlify
+.redwood/*
+!.redwood/README.md
+dev.db*
+dist
+dist-babel
+node_modules
+yarn-error.log
+web/public/mockServiceWorker.js
+web/types/graphql.d.ts
+api/types/graphql.d.ts
+api/src/lib/generateGraphiQLHeader.*
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
diff --git a/__fixtures__/fragment-test-project/.redwood/README.md b/__fixtures__/fragment-test-project/.redwood/README.md
new file mode 100644
index 000000000000..f22b586a47cc
--- /dev/null
+++ b/__fixtures__/fragment-test-project/.redwood/README.md
@@ -0,0 +1,44 @@
+# .redwood
+
+## What is this directory?
+
+Redwood uses this `.redwood` directory to store transitory data that aids in the smooth and convenient operation of your Redwood project.
+
+## Do I need to do anything with this directory?
+
+No. You shouldn't have to create, edit or delete anything in this directory in your day-to-day work with Redwood.
+
+You don't need to commit any other contents of this directory to your version control system. It's ignored by default.
+
+## What's in this directory?
+
+### Files
+
+| Name | Description |
+| :---------------- | :------- |
+| commandCache.json | This file contains mappings to assist the Redwood CLI in efficiently executing commands. |
+| schema.graphql | This is the GraphQL schema which has been automatically generated from your Redwood project. |
+| studio.db | The sqlite database used by the experimental `rw exp studio` feature. |
+| telemetry.txt | Contains a unique ID used for telemetry. This value is rotated every 24 hours to protect your project's anonymity. |
+| test.db | The sqlite database used when running tests. |
+
+### Directories
+
+| Name | Description |
+| :---------- | :------- |
+| locks | Stores temporary files that Redwood uses to keep track of the execution of async/background tasks between processes. |
+| logs | Stores log files for background tasks such as update checking. |
+| prebuild | Stores transpiled JavaScript that is generated as part of Redwood's build process. |
+| telemetry | Stores the recent telemetry that the Redwood CLI has generated. You may inspect these files to see everything Redwood is anonymously collecting. |
+| types | Stores the results of type generation. |
+| updateCheck | Stores a file which contains the results of checking for Redwood updates. |
+
+We try to keep this README up to date but you may, from time to time, find other files or directories in this `.redwood` directory that have not yet been documented here. This is likely nothing to worry about but feel free to let us know and we'll update this list.
+
+### Telemetry
+
+RedwoodJS collects completely anonymous telemetry data about general usage. For transparency, that data is viewable in the respective directories and files. To learn more and manage your project's settings, visit [telemetry.redwoodjs.com](https://telemetry.redwoodjs.com).
+
+### Have any questions?
+
+Feel free to reach out to us in the [RedwoodJS Community](https://community.redwoodjs.com/) forum if you have any questions.
diff --git a/__fixtures__/fragment-test-project/.redwood/schema.graphql b/__fixtures__/fragment-test-project/.redwood/schema.graphql
deleted file mode 100644
index 1cd2ea849879..000000000000
--- a/__fixtures__/fragment-test-project/.redwood/schema.graphql
+++ /dev/null
@@ -1,102 +0,0 @@
-"""
-Use to check whether or not a user is authenticated and is associated
-with an optional set of roles.
-"""
-directive @requireAuth(roles: [String]) on FIELD_DEFINITION
-
-"""Use to skip authentication checks and allow public access."""
-directive @skipAuth on FIELD_DEFINITION
-
-scalar BigInt
-
-scalar Date
-
-scalar DateTime
-
-type Fruit implements Grocery {
- id: ID!
-
- """Seedless is only for fruits"""
- isSeedless: Boolean
- name: String!
- nutrients: String
- price: Int!
- quantity: Int!
- region: String!
-
- """Ripeness is only for fruits"""
- ripenessIndicators: String
- stall: Stall!
-}
-
-union Groceries = Fruit | Vegetable
-
-interface Grocery {
- id: ID!
- name: String!
- nutrients: String
- price: Int!
- quantity: Int!
- region: String!
- stall: Stall!
-}
-
-scalar JSON
-
-scalar JSONObject
-
-"""About the Redwood queries."""
-type Query {
- fruitById(id: ID!): Fruit
- fruits: [Fruit!]!
- groceries: [Groceries!]!
-
- """Fetches the Redwood root schema."""
- redwood: Redwood
- stallById(id: ID!): Stall
- stalls: [Stall!]!
- vegetableById(id: ID!): Vegetable
- vegetables: [Vegetable!]!
-}
-
-"""
-The RedwoodJS Root Schema
-
-Defines details about RedwoodJS such as the current user and version information.
-"""
-type Redwood {
- """The current user."""
- currentUser: JSON
-
- """The version of Prisma."""
- prismaVersion: String
-
- """The version of Redwood."""
- version: String
-}
-
-type Stall {
- fruits: [Fruit]
- id: ID!
- name: String!
- stallNumber: String!
- vegetables: [Vegetable]
-}
-
-scalar Time
-
-type Vegetable implements Grocery {
- id: ID!
-
- """Pickled is only for vegetables"""
- isPickled: Boolean
- name: String!
- nutrients: String
- price: Int!
- quantity: Int!
- region: String!
- stall: Stall!
-
- """Veggie Family is only for vegetables"""
- vegetableFamily: String
-}
\ No newline at end of file
diff --git a/__fixtures__/fragment-test-project/.vscode/extensions.json b/__fixtures__/fragment-test-project/.vscode/extensions.json
new file mode 100644
index 000000000000..6e458a923135
--- /dev/null
+++ b/__fixtures__/fragment-test-project/.vscode/extensions.json
@@ -0,0 +1,16 @@
+{
+ "recommendations": [
+ "dbaeumer.vscode-eslint",
+ "eamodio.gitlens",
+ "ofhumanbondage.react-proptypes-intellisense",
+ "mgmcdermott.vscode-language-babel",
+ "wix.vscode-import-cost",
+ "pflannery.vscode-versionlens",
+ "editorconfig.editorconfig",
+ "prisma.prisma",
+ "graphql.vscode-graphql",
+ "csstools.postcss",
+ "bradlc.vscode-tailwindcss"
+ ],
+ "unwantedRecommendations": []
+}
\ No newline at end of file
diff --git a/__fixtures__/fragment-test-project/.vscode/launch.json b/__fixtures__/fragment-test-project/.vscode/launch.json
new file mode 100644
index 000000000000..340be43c34da
--- /dev/null
+++ b/__fixtures__/fragment-test-project/.vscode/launch.json
@@ -0,0 +1,56 @@
+{
+ "version": "0.3.0",
+ "configurations": [
+ {
+ "command": "yarn redwood dev --apiDebugPort 18911", // you can add --fwd='--open=false' to prevent the browser from opening
+ "name": "Run Dev Server",
+ "request": "launch",
+ "type": "node-terminal"
+ },
+ {
+ "name": "Attach API debugger",
+ "port": 18911, // you can change this port, see https://redwoodjs.com/docs/project-configuration-dev-test-build#debugger-configuration
+ "request": "attach",
+ "skipFiles": [
+ "/**"
+ ],
+ "type": "node",
+ "localRoot": "${workspaceFolder}/node_modules/@redwoodjs/api-server/dist",
+ "remoteRoot": "${workspaceFolder}/node_modules/@redwoodjs/api-server/dist",
+ "sourceMaps": true,
+ "restart": true,
+ "preLaunchTask": "WaitForDevServer",
+ },
+ {
+ "name": "Launch Web debugger",
+ "type": "chrome",
+ "request": "launch",
+ "url": "http://localhost:8910",
+ "webRoot": "${workspaceRoot}/web/src",
+ "preLaunchTask": "WaitForDevServer",
+ },
+ {
+ "command": "yarn redwood test api",
+ "name": "Test api",
+ "request": "launch",
+ "type": "node-terminal"
+ },
+ {
+ "command": "yarn redwood test web",
+ "name": "Test web",
+ "request": "launch",
+ "type": "node-terminal"
+ },
+ ],
+ "compounds": [
+ {
+ "name": "Start Debug",
+ "configurations": [
+ "Run Dev Server",
+ "Attach API debugger",
+ "Launch Web debugger"
+ ],
+ "stopAll": true
+ }
+ ]
+}
diff --git a/__fixtures__/fragment-test-project/.vscode/settings.json b/__fixtures__/fragment-test-project/.vscode/settings.json
new file mode 100644
index 000000000000..6887d360eb96
--- /dev/null
+++ b/__fixtures__/fragment-test-project/.vscode/settings.json
@@ -0,0 +1,11 @@
+{
+ "editor.tabSize": 2,
+ "files.trimTrailingWhitespace": true,
+ "editor.formatOnSave": false,
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": "explicit"
+ },
+ "[prisma]": {
+ "editor.formatOnSave": true
+ }
+}
diff --git a/__fixtures__/fragment-test-project/.vscode/tasks.json b/__fixtures__/fragment-test-project/.vscode/tasks.json
new file mode 100644
index 000000000000..549249ec6324
--- /dev/null
+++ b/__fixtures__/fragment-test-project/.vscode/tasks.json
@@ -0,0 +1,29 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "WaitForDevServer",
+ "group": "none",
+ "type": "shell",
+ "command": "bash",
+ "args": [
+ "-c",
+ "while ! echo -n > /dev/tcp/localhost/18911; do sleep 1; done;"
+ ],
+ "windows": {
+ "command": "powershell",
+ "args": [
+ "-NoProfile",
+ "-ExecutionPolicy", "Bypass",
+ "while (-not (Test-NetConnection -ComputerName localhost -Port 18911)) { Start-Sleep -Seconds 1 };"
+ ]
+ },
+ "presentation": {
+ "reveal": "silent",
+ "revealProblems": "onProblem",
+ "panel": "shared",
+ "close": true
+ }
+ },
+ ]
+}
diff --git a/__fixtures__/fragment-test-project/.yarnrc.yml b/__fixtures__/fragment-test-project/.yarnrc.yml
new file mode 100644
index 000000000000..e8c5d50aa786
--- /dev/null
+++ b/__fixtures__/fragment-test-project/.yarnrc.yml
@@ -0,0 +1,15 @@
+# Yarn's manifest file. You can configure yarn here.
+# See https://yarnpkg.com/configuration/yarnrc.
+
+# For `node_modules` (see `nodeLinker` below), this is almost always the preferred option.
+compressionLevel: 0
+
+enableGlobalCache: true
+
+# Lets yarn use hardlinks inside `node_modules` to dedupe packages.
+# For a more pnpm-like experience, consider `hardlinks-global` where hardlinks point to a global store.
+nmMode: hardlinks-local
+
+# How to install Node packages.
+# Heads up: right now, Redwood expects this to be `node-modules`.
+nodeLinker: node-modules
diff --git a/__fixtures__/fragment-test-project/README.md b/__fixtures__/fragment-test-project/README.md
index 861af7d9e65e..60a38fe2c6ac 100644
--- a/__fixtures__/fragment-test-project/README.md
+++ b/__fixtures__/fragment-test-project/README.md
@@ -1,4 +1,122 @@
-# Project to Test GraphQL Fragments
+# README
-* Has unions and interfaces to test: https://www.apollographql.com/docs/react/data/fragments/#using-fragments-with-unions-and-interfaces
-* Generates "possible types" for Apollo Client, see: https://www.apollographql.com/docs/react/data/fragments/#defining-possibletypes-manually
+Welcome to [RedwoodJS](https://redwoodjs.com)!
+
+> **Prerequisites**
+>
+> - Redwood requires [Node.js](https://nodejs.org/en/) (=20.x) and [Yarn](https://yarnpkg.com/)
+> - Are you on Windows? For best results, follow our [Windows development setup](https://redwoodjs.com/docs/how-to/windows-development-setup) guide
+
+Start by installing dependencies:
+
+```
+yarn install
+```
+
+Then start the development server:
+
+```
+yarn redwood dev
+```
+
+Your browser should automatically open to [http://localhost:8910](http://localhost:8910) where you'll see the Welcome Page, which links out to many great resources.
+
+> **The Redwood CLI**
+>
+> Congratulations on running your first Redwood CLI command! From dev to deploy, the CLI is with you the whole way. And there's quite a few commands at your disposal:
+>
+> ```
+> yarn redwood --help
+> ```
+>
+> For all the details, see the [CLI reference](https://redwoodjs.com/docs/cli-commands).
+
+## Prisma and the database
+
+Redwood wouldn't be a full-stack framework without a database. It all starts with the schema. Open the [`schema.prisma`](api/db/schema.prisma) file in `api/db` and replace the `UserExample` model with the following `Post` model:
+
+```prisma
+model Post {
+ id Int @id @default(autoincrement())
+ title String
+ body String
+ createdAt DateTime @default(now())
+}
+```
+
+Redwood uses [Prisma](https://www.prisma.io/), a next-gen Node.js and TypeScript ORM, to talk to the database. Prisma's schema offers a declarative way of defining your app's data models. And Prisma [Migrate](https://www.prisma.io/migrate) uses that schema to make database migrations hassle-free:
+
+```
+yarn rw prisma migrate dev
+
+# ...
+
+? Enter a name for the new migration: βΊ create posts
+```
+
+> `rw` is short for `redwood`
+
+You'll be prompted for the name of your migration. `create posts` will do.
+
+Now let's generate everything we need to perform all the CRUD (Create, Retrieve, Update, Delete) actions on our `Post` model:
+
+```
+yarn redwood generate scaffold post
+```
+
+Navigate to [http://localhost:8910/posts/new](http://localhost:8910/posts/new), fill in the title and body, and click "Save".
+
+Did we just create a post in the database? Yup! With `yarn rw generate scaffold `, Redwood created all the pages, components, and services necessary to perform all CRUD actions on our posts table.
+
+## Frontend first with Storybook
+
+Don't know what your data models look like? That's more than okβRedwood integrates Storybook so that you can work on design without worrying about data. Mockup, build, and verify your React components, even in complete isolation from the backend:
+
+```
+yarn rw storybook
+```
+
+Seeing "Couldn't find any stories"? That's because you need a `*.stories.{tsx,jsx}` file. The Redwood CLI makes getting one easy enoughβtry generating a [Cell](https://redwoodjs.com/docs/cells), Redwood's data-fetching abstraction:
+
+```
+yarn rw generate cell examplePosts
+```
+
+The Storybook server should hot reload and now you'll have four stories to work with. They'll probably look a little bland since there's no styling. See if the Redwood CLI's `setup ui` command has your favorite styling library:
+
+```
+yarn rw setup ui --help
+```
+
+## Testing with Jest
+
+It'd be hard to scale from side project to startup without a few tests. Redwood fully integrates Jest with both the front- and back-ends, and makes it easy to keep your whole app covered by generating test files with all your components and services:
+
+```
+yarn rw test
+```
+
+To make the integration even more seamless, Redwood augments Jest with database [scenarios](https://redwoodjs.com/docs/testing#scenarios) and [GraphQL mocking](https://redwoodjs.com/docs/testing#mocking-graphql-calls).
+
+## Ship it
+
+Redwood is designed for both serverless deploy targets like Netlify and Vercel and serverful deploy targets like Render and AWS:
+
+```
+yarn rw setup deploy --help
+```
+
+Don't go live without auth! Lock down your app with Redwood's built-in, database-backed authentication system ([dbAuth](https://redwoodjs.com/docs/authentication#self-hosted-auth-installation-and-setup)), or integrate with nearly a dozen third-party auth providers:
+
+```
+yarn rw setup auth --help
+```
+
+## Next Steps
+
+The best way to learn Redwood is by going through the comprehensive [tutorial](https://redwoodjs.com/docs/tutorial/foreword) and joining the community (via the [Discourse forum](https://community.redwoodjs.com) or the [Discord server](https://discord.gg/redwoodjs)).
+
+## Quick Links
+
+- Stay updated: read [Forum announcements](https://community.redwoodjs.com/c/announcements/5), follow us on [Twitter](https://twitter.com/redwoodjs), and subscribe to the [newsletter](https://redwoodjs.com/newsletter)
+- [Learn how to contribute](https://redwoodjs.com/docs/contributing)
diff --git a/__fixtures__/fragment-test-project/api/db/migrations/20220101120000_create_post_user/migration.sql b/__fixtures__/fragment-test-project/api/db/migrations/20220101120000_create_post_user/migration.sql
new file mode 100644
index 000000000000..9dd73df9b6ac
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/db/migrations/20220101120000_create_post_user/migration.sql
@@ -0,0 +1,34 @@
+-- CreateTable
+CREATE TABLE "UserExample" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "email" TEXT NOT NULL,
+ "name" TEXT
+);
+
+-- CreateTable
+CREATE TABLE "Post" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "title" TEXT NOT NULL,
+ "body" TEXT NOT NULL,
+ "authorId" INTEGER NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "User" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "email" TEXT NOT NULL,
+ "hashedPassword" TEXT NOT NULL,
+ "fullName" TEXT NOT NULL,
+ "salt" TEXT NOT NULL,
+ "resetToken" TEXT,
+ "resetTokenExpiresAt" DATETIME,
+ "roles" TEXT
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UserExample_email_key" ON "UserExample"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
diff --git a/__fixtures__/fragment-test-project/api/db/migrations/20220102120000_create_contact/migration.sql b/__fixtures__/fragment-test-project/api/db/migrations/20220102120000_create_contact/migration.sql
new file mode 100644
index 000000000000..8d7bd91beb4d
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/db/migrations/20220102120000_create_contact/migration.sql
@@ -0,0 +1,8 @@
+-- CreateTable
+CREATE TABLE "Contact" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "name" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "message" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
diff --git a/__fixtures__/fragment-test-project/api/db/migrations/20240106111257_create_produce_stall/migration.sql b/__fixtures__/fragment-test-project/api/db/migrations/20240106111257_create_produce_stall/migration.sql
new file mode 100644
index 000000000000..f7f6352f3e01
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/db/migrations/20240106111257_create_produce_stall/migration.sql
@@ -0,0 +1,28 @@
+-- CreateTable
+CREATE TABLE "Produce" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "name" TEXT NOT NULL,
+ "quantity" INTEGER NOT NULL,
+ "price" INTEGER NOT NULL,
+ "nutrients" TEXT,
+ "region" TEXT NOT NULL,
+ "isSeedless" BOOLEAN,
+ "ripenessIndicators" TEXT,
+ "vegetableFamily" TEXT,
+ "isPickled" BOOLEAN,
+ "stallId" TEXT NOT NULL,
+ CONSTRAINT "Produce_stallId_fkey" FOREIGN KEY ("stallId") REFERENCES "Stall" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "Stall" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "name" TEXT NOT NULL,
+ "stallNumber" TEXT NOT NULL
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Produce_name_key" ON "Produce"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Stall_stallNumber_key" ON "Stall"("stallNumber");
diff --git a/__fixtures__/fragment-test-project/api/db/migrations/migration_lock.toml b/__fixtures__/fragment-test-project/api/db/migrations/migration_lock.toml
new file mode 100644
index 000000000000..e5e5c4705ab0
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/db/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (i.e. Git)
+provider = "sqlite"
\ No newline at end of file
diff --git a/__fixtures__/fragment-test-project/api/db/schema.prisma b/__fixtures__/fragment-test-project/api/db/schema.prisma
index 20b2df54078c..bf7e70169a1b 100644
--- a/__fixtures__/fragment-test-project/api/db/schema.prisma
+++ b/__fixtures__/fragment-test-project/api/db/schema.prisma
@@ -1,7 +1,12 @@
+// Don't forget to tell Prisma about your edits to this file using
+// `yarn rw prisma migrate dev` or `yarn rw prisma db push`.
+// `migrate` is like committing while `push` is for prototyping.
+// Read more about both here:
+// https://www.prisma.io/docs/orm/prisma-migrate
+
datasource db {
- provider = "sqlite"
- url = env("DATABASE_URL")
- directUrl = env("DIRECT_URL")
+ provider = "sqlite"
+ url = env("DATABASE_URL")
}
generator client {
@@ -9,11 +14,42 @@ generator client {
binaryTargets = "native"
}
-model Stall {
- id String @id @default(cuid())
- name String
- stallNumber String @unique
- produce Produce[]
+// Define your own datamodels here and run `yarn redwood prisma migrate dev`
+// to create migrations for them and apply to your dev DB.
+// TODO: Please remove the following example:
+model UserExample {
+ id Int @id @default(autoincrement())
+ email String @unique
+ name String?
+}
+
+model Post {
+ id Int @id @default(autoincrement())
+ title String
+ body String
+ authorId Int
+ author User @relation(fields: [authorId], references: [id])
+ createdAt DateTime @default(now())
+}
+
+model User {
+ id Int @id @default(autoincrement())
+ email String @unique
+ hashedPassword String
+ fullName String
+ salt String
+ resetToken String?
+ resetTokenExpiresAt DateTime?
+ roles String?
+ posts Post[]
+}
+
+model Contact {
+ id Int @id @default(autoincrement())
+ name String
+ email String
+ message String
+ createdAt DateTime @default(now())
}
model Produce {
@@ -34,3 +70,10 @@ model Produce {
stall Stall @relation(fields: [stallId], references: [id], onDelete: Cascade)
stallId String
}
+
+model Stall {
+ id String @id @default(cuid())
+ name String
+ stallNumber String @unique
+ produce Produce[]
+}
diff --git a/__fixtures__/fragment-test-project/api/package.json b/__fixtures__/fragment-test-project/api/package.json
index 4a05d2980285..1fe562e03fbe 100644
--- a/__fixtures__/fragment-test-project/api/package.json
+++ b/__fixtures__/fragment-test-project/api/package.json
@@ -3,7 +3,8 @@
"version": "0.0.0",
"private": true,
"dependencies": {
- "@redwoodjs/api": "6.2.0",
- "@redwoodjs/graphql-server": "6.2.0"
+ "@redwoodjs/api": "6.0.7",
+ "@redwoodjs/auth-dbauth-api": "6.0.7",
+ "@redwoodjs/graphql-server": "6.0.7"
}
}
diff --git a/__fixtures__/fragment-test-project/api/src/.keep b/__fixtures__/fragment-test-project/api/src/.keep
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/__fixtures__/fragment-test-project/api/src/__tests__/context.test.ts b/__fixtures__/fragment-test-project/api/src/__tests__/context.test.ts
new file mode 100644
index 000000000000..972c4756e85d
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/__tests__/context.test.ts
@@ -0,0 +1,14 @@
+test('Set a mock user on the context', async () => {
+ const user = {
+ id: 0o7,
+ name: 'Bond, James Bond',
+ email: 'totallyNotASpy@example.com',
+ roles: 'secret_agent',
+ }
+ mockCurrentUser(user)
+ expect(context.currentUser).toStrictEqual(user)
+})
+
+test('Context is isolated between tests', () => {
+ expect(context).toStrictEqual({})
+})
diff --git a/__fixtures__/fragment-test-project/api/src/directives/requireAuth/requireAuth.test.ts b/__fixtures__/fragment-test-project/api/src/directives/requireAuth/requireAuth.test.ts
index 0f01aa367a85..abd5e1864a95 100644
--- a/__fixtures__/fragment-test-project/api/src/directives/requireAuth/requireAuth.test.ts
+++ b/__fixtures__/fragment-test-project/api/src/directives/requireAuth/requireAuth.test.ts
@@ -11,7 +11,9 @@ describe('requireAuth directive', () => {
it('requireAuth has stub implementation. Should not throw when current user', () => {
// If you want to set values in context, pass it through e.g.
// mockRedwoodDirective(requireAuth, { context: { currentUser: { id: 1, name: 'Lebron McGretzky' } }})
- const mockExecution = mockRedwoodDirective(requireAuth, { context: {} })
+ const mockExecution = mockRedwoodDirective(requireAuth, {
+ context: { currentUser: { id: 1, roles: 'ADMIN', email: 'b@zinga.com' } },
+ })
expect(mockExecution).not.toThrowError()
})
diff --git a/__fixtures__/fragment-test-project/api/src/functions/auth.ts b/__fixtures__/fragment-test-project/api/src/functions/auth.ts
new file mode 100644
index 000000000000..d71b437e9802
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/functions/auth.ts
@@ -0,0 +1,202 @@
+import type { APIGatewayProxyEvent, Context } from 'aws-lambda'
+
+import { DbAuthHandler } from '@redwoodjs/auth-dbauth-api'
+import type { DbAuthHandlerOptions, UserType } from '@redwoodjs/auth-dbauth-api'
+
+import { cookieName } from 'src/lib/auth'
+import { db } from 'src/lib/db'
+
+export const handler = async (
+ event: APIGatewayProxyEvent,
+ context: Context
+) => {
+ const forgotPasswordOptions: DbAuthHandlerOptions['forgotPassword'] = {
+ // handler() is invoked after verifying that a user was found with the given
+ // username. This is where you can send the user an email with a link to
+ // reset their password. With the default dbAuth routes and field names, the
+ // URL to reset the password will be:
+ //
+ // https://example.com/reset-password?resetToken=${user.resetToken}
+ //
+ // Whatever is returned from this function will be returned from
+ // the `forgotPassword()` function that is destructured from `useAuth()`.
+ // You could use this return value to, for example, show the email
+ // address in a toast message so the user will know it worked and where
+ // to look for the email.
+ //
+ // Note that this return value is sent to the client in *plain text*
+ // so don't include anything you wouldn't want prying eyes to see. The
+ // `user` here has been sanitized to only include the fields listed in
+ // `allowedUserFields` so it should be safe to return as-is.
+ handler: (user, _resetToken) => {
+ // TODO: Send user an email/message with a link to reset their password,
+ // including the `resetToken`. The URL should look something like:
+ // `http://localhost:8910/reset-password?resetToken=${resetToken}`
+
+ return user
+ },
+
+ // How long the resetToken is valid for, in seconds (default is 24 hours)
+ expires: 60 * 60 * 24,
+
+ errors: {
+ // for security reasons you may want to be vague here rather than expose
+ // the fact that the email address wasn't found (prevents fishing for
+ // valid email addresses)
+ usernameNotFound: 'Username not found',
+ // if the user somehow gets around client validation
+ usernameRequired: 'Username is required',
+ },
+ }
+
+ const loginOptions: DbAuthHandlerOptions['login'] = {
+ // handler() is called after finding the user that matches the
+ // username/password provided at login, but before actually considering them
+ // logged in. The `user` argument will be the user in the database that
+ // matched the username/password.
+ //
+ // If you want to allow this user to log in simply return the user.
+ //
+ // If you want to prevent someone logging in for another reason (maybe they
+ // didn't validate their email yet), throw an error and it will be returned
+ // by the `logIn()` function from `useAuth()` in the form of:
+ // `{ message: 'Error message' }`
+ handler: (user) => {
+ return user
+ },
+
+ errors: {
+ usernameOrPasswordMissing: 'Both username and password are required',
+ usernameNotFound: 'Username ${username} not found',
+ // For security reasons you may want to make this the same as the
+ // usernameNotFound error so that a malicious user can't use the error
+ // to narrow down if it's the username or password that's incorrect
+ incorrectPassword: 'Incorrect password for ${username}',
+ },
+
+ // How long a user will remain logged in, in seconds
+ expires: 60 * 60 * 24 * 365 * 10,
+ }
+
+ const resetPasswordOptions: DbAuthHandlerOptions['resetPassword'] = {
+ // handler() is invoked after the password has been successfully updated in
+ // the database. Returning anything truthy will automatically log the user
+ // in. Return `false` otherwise, and in the Reset Password page redirect the
+ // user to the login page.
+ handler: (_user) => {
+ return true
+ },
+
+ // If `false` then the new password MUST be different from the current one
+ allowReusedPassword: true,
+
+ errors: {
+ // the resetToken is valid, but expired
+ resetTokenExpired: 'resetToken is expired',
+ // no user was found with the given resetToken
+ resetTokenInvalid: 'resetToken is invalid',
+ // the resetToken was not present in the URL
+ resetTokenRequired: 'resetToken is required',
+ // new password is the same as the old password (apparently they did not forget it)
+ reusedPassword: 'Must choose a new password',
+ },
+ }
+
+ interface UserAttributes {
+ 'full-name': string
+ }
+
+ const signupOptions: DbAuthHandlerOptions<
+ UserType,
+ UserAttributes
+ >['signup'] = {
+ // Whatever you want to happen to your data on new user signup. Redwood will
+ // check for duplicate usernames before calling this handler. At a minimum
+ // you need to save the `username`, `hashedPassword` and `salt` to your
+ // user table. `userAttributes` contains any additional object members that
+ // were included in the object given to the `signUp()` function you got
+ // from `useAuth()`.
+ //
+ // If you want the user to be immediately logged in, return the user that
+ // was created.
+ //
+ // If this handler throws an error, it will be returned by the `signUp()`
+ // function in the form of: `{ error: 'Error message' }`.
+ //
+ // If this returns anything else, it will be returned by the
+ // `signUp()` function in the form of: `{ message: 'String here' }`.
+ handler: ({ username, hashedPassword, salt, userAttributes }) => {
+ return db.user.create({
+ data: {
+ email: username,
+ hashedPassword: hashedPassword,
+ salt: salt,
+ fullName: userAttributes['full-name'],
+ },
+ })
+ },
+
+ // Include any format checks for password here. Return `true` if the
+ // password is valid, otherwise throw a `PasswordValidationError`.
+ // Import the error along with `DbAuthHandler` from `@redwoodjs/api` above.
+ passwordValidation: (_password) => {
+ return true
+ },
+
+ errors: {
+ // `field` will be either "username" or "password"
+ fieldMissing: '${field} is required',
+ usernameTaken: 'Username `${username}` already in use',
+ },
+ }
+
+ const authHandler = new DbAuthHandler(event, context, {
+ // Provide prisma db client
+ db: db,
+
+ // The name of the property you'd call on `db` to access your user table.
+ // i.e. if your Prisma model is named `User` this value would be `user`, as in `db.user`
+ authModelAccessor: 'user',
+
+ // A map of what dbAuth calls a field to what your database calls it.
+ // `id` is whatever column you use to uniquely identify a user (probably
+ // something like `id` or `userId` or even `email`)
+ authFields: {
+ id: 'id',
+ username: 'email',
+ hashedPassword: 'hashedPassword',
+ salt: 'salt',
+ resetToken: 'resetToken',
+ resetTokenExpiresAt: 'resetTokenExpiresAt',
+ },
+
+ // A list of fields on your user object that are safe to return to the
+ // client when invoking a handler that returns a user (like forgotPassword
+ // and signup). This list should be as small as possible to be sure not to
+ // leak any sensitive information to the client.
+ allowedUserFields: ['id', 'email'],
+
+ // Specifies attributes on the cookie that dbAuth sets in order to remember
+ // who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
+ cookie: {
+ attributes: {
+ HttpOnly: true,
+ Path: '/',
+ SameSite: 'Strict',
+ Secure: process.env.NODE_ENV !== 'development',
+
+ // If you need to allow other domains (besides the api side) access to
+ // the dbAuth session cookie:
+ // Domain: 'example.com',
+ },
+ name: cookieName,
+ },
+
+ forgotPassword: forgotPasswordOptions,
+ login: loginOptions,
+ resetPassword: resetPasswordOptions,
+ signup: signupOptions,
+ })
+
+ return await authHandler.invoke()
+}
diff --git a/__fixtures__/fragment-test-project/api/src/functions/graphql.ts b/__fixtures__/fragment-test-project/api/src/functions/graphql.ts
index f395c3b0f852..e9c53e285fad 100644
--- a/__fixtures__/fragment-test-project/api/src/functions/graphql.ts
+++ b/__fixtures__/fragment-test-project/api/src/functions/graphql.ts
@@ -1,13 +1,19 @@
+import { createAuthDecoder } from '@redwoodjs/auth-dbauth-api'
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
+import { cookieName, getCurrentUser } from 'src/lib/auth'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'
+const authDecoder = createAuthDecoder(cookieName)
+
export const handler = createGraphQLHandler({
+ authDecoder,
+ getCurrentUser,
loggerConfig: { logger, options: {} },
directives,
sdls,
diff --git a/__fixtures__/fragment-test-project/api/src/graphql/contacts.sdl.ts b/__fixtures__/fragment-test-project/api/src/graphql/contacts.sdl.ts
new file mode 100644
index 000000000000..7dec262a579b
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/graphql/contacts.sdl.ts
@@ -0,0 +1,32 @@
+export const schema = gql`
+ type Contact {
+ id: Int!
+ name: String!
+ email: String!
+ message: String!
+ createdAt: DateTime!
+ }
+
+ type Query {
+ contacts: [Contact!]! @requireAuth
+ contact(id: Int!): Contact @requireAuth
+ }
+
+ input CreateContactInput {
+ name: String!
+ email: String!
+ message: String!
+ }
+
+ input UpdateContactInput {
+ name: String
+ email: String
+ message: String
+ }
+
+ type Mutation {
+ createContact(input: CreateContactInput!): Contact @skipAuth
+ updateContact(id: Int!, input: UpdateContactInput!): Contact! @requireAuth
+ deleteContact(id: Int!): Contact! @requireAuth(roles: ["ADMIN"])
+ }
+`
diff --git a/__fixtures__/fragment-test-project/api/src/graphql/posts.sdl.ts b/__fixtures__/fragment-test-project/api/src/graphql/posts.sdl.ts
new file mode 100644
index 000000000000..09cf9b2cc6b2
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/graphql/posts.sdl.ts
@@ -0,0 +1,33 @@
+export const schema = gql`
+ type Post {
+ id: Int!
+ title: String!
+ body: String!
+ authorId: Int!
+ author: User!
+ createdAt: DateTime!
+ }
+
+ type Query {
+ posts: [Post!]! @skipAuth
+ post(id: Int!): Post @skipAuth
+ }
+
+ input CreatePostInput {
+ title: String!
+ body: String!
+ authorId: Int!
+ }
+
+ input UpdatePostInput {
+ title: String
+ body: String
+ authorId: Int
+ }
+
+ type Mutation {
+ createPost(input: CreatePostInput!): Post! @requireAuth
+ updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth
+ deletePost(id: Int!): Post! @requireAuth
+ }
+`
diff --git a/__fixtures__/fragment-test-project/api/src/graphql/produces.sdl.ts b/__fixtures__/fragment-test-project/api/src/graphql/produces.sdl.ts
new file mode 100644
index 000000000000..1a3342f66270
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/graphql/produces.sdl.ts
@@ -0,0 +1,54 @@
+export const schema = gql`
+ type Produce {
+ id: String!
+ name: String!
+ quantity: Int!
+ price: Int!
+ nutrients: String
+ region: String!
+ isSeedless: Boolean
+ ripenessIndicators: String
+ vegetableFamily: String
+ isPickled: Boolean
+ stall: Stall!
+ stallId: String!
+ }
+
+ type Query {
+ produces: [Produce!]! @skipAuth
+ produce(id: String!): Produce @skipAuth
+ }
+
+ input CreateProduceInput {
+ name: String!
+ quantity: Int!
+ price: Int!
+ nutrients: String
+ region: String!
+ isSeedless: Boolean
+ ripenessIndicators: String
+ vegetableFamily: String
+ isPickled: Boolean
+ stallId: String!
+ }
+
+ input UpdateProduceInput {
+ name: String
+ quantity: Int
+ price: Int
+ nutrients: String
+ region: String
+ isSeedless: Boolean
+ ripenessIndicators: String
+ vegetableFamily: String
+ isPickled: Boolean
+ stallId: String
+ }
+
+ type Mutation {
+ createProduce(input: CreateProduceInput!): Produce! @skipAuth
+ updateProduce(id: String!, input: UpdateProduceInput!): Produce!
+ @skipAuth
+ deleteProduce(id: String!): Produce! @skipAuth
+ }
+`
diff --git a/__fixtures__/fragment-test-project/api/src/graphql/stalls.sdl.ts b/__fixtures__/fragment-test-project/api/src/graphql/stalls.sdl.ts
index ff159aa2df9a..c934eeb4a5fd 100644
--- a/__fixtures__/fragment-test-project/api/src/graphql/stalls.sdl.ts
+++ b/__fixtures__/fragment-test-project/api/src/graphql/stalls.sdl.ts
@@ -1,14 +1,29 @@
export const schema = gql`
type Stall {
- id: ID!
- stallNumber: String!
+ id: String!
name: String!
- fruits: [Fruit]
- vegetables: [Vegetable]
+ stallNumber: String!
+ produce: [Produce]!
}
type Query {
- stalls: [Stall!]! @skipAuth
- stallById(id: ID!): Stall @skipAuth
+ stalls: [Stall!]! @requireAuth
+ stall(id: String!): Stall @requireAuth
+ }
+
+ input CreateStallInput {
+ name: String!
+ stallNumber: String!
+ }
+
+ input UpdateStallInput {
+ name: String
+ stallNumber: String
+ }
+
+ type Mutation {
+ createStall(input: CreateStallInput!): Stall! @requireAuth
+ updateStall(id: String!, input: UpdateStallInput!): Stall! @requireAuth
+ deleteStall(id: String!): Stall! @requireAuth
}
`
diff --git a/__fixtures__/fragment-test-project/api/src/graphql/users.sdl.ts b/__fixtures__/fragment-test-project/api/src/graphql/users.sdl.ts
new file mode 100644
index 000000000000..e2d1c0bed1d1
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/graphql/users.sdl.ts
@@ -0,0 +1,25 @@
+export const schema = gql`
+ type User {
+ id: Int!
+ email: String!
+ fullName: String!
+ roles: String
+ posts: [Post]!
+ }
+
+ type Query {
+ user(id: Int!): User @skipAuth
+ }
+
+ input CreateUserInput {
+ email: String!
+ fullName: String!
+ roles: String
+ }
+
+ input UpdateUserInput {
+ email: String
+ fullName: String
+ roles: String
+ }
+`
diff --git a/__fixtures__/fragment-test-project/api/src/lib/auth.ts b/__fixtures__/fragment-test-project/api/src/lib/auth.ts
index f98fe93a960c..4e8f9005ebe5 100644
--- a/__fixtures__/fragment-test-project/api/src/lib/auth.ts
+++ b/__fixtures__/fragment-test-project/api/src/lib/auth.ts
@@ -1,25 +1,121 @@
+import type { Decoded } from '@redwoodjs/api'
+import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'
+
+import { db } from './db'
+
+/**
+ * The name of the cookie that dbAuth sets
+ *
+ * %port% will be replaced with the port the api server is running on.
+ * If you have multiple RW apps running on the same host, you'll need to
+ * make sure they all use unique cookie names
+ */
+export const cookieName = 'session_%port%'
+
/**
- * Once you are ready to add authentication to your application
- * you'll build out requireAuth() with real functionality. For
- * now we just return `true` so that the calls in services
- * have something to check against, simulating a logged
- * in user that is allowed to access that service.
+ * The session object sent in as the first argument to getCurrentUser() will
+ * have a single key `id` containing the unique ID of the logged in user
+ * (whatever field you set as `authFields.id` in your auth function config).
+ * You'll need to update the call to `db` below if you use a different model
+ * name or unique field name, for example:
*
- * See https://redwoodjs.com/docs/authentication for more info.
+ * return await db.profile.findUnique({ where: { email: session.id } })
+ * ββββ¬βββ βββ¬ββ
+ * model accessor ββ unique id field name ββ
+ *
+ * !! BEWARE !! Anything returned from this function will be available to the
+ * client--it becomes the content of `currentUser` on the web side (as well as
+ * `context.currentUser` on the api side). You should carefully add additional
+ * fields to the `select` object below once you've decided they are safe to be
+ * seen if someone were to open the Web Inspector in their browser.
*/
-export const isAuthenticated = () => {
- return true
+export const getCurrentUser = async (session: Decoded) => {
+ if (!session || typeof session.id !== 'number') {
+ throw new Error('Invalid session')
+ }
+
+ return await db.user.findUnique({
+ where: { id: session.id },
+ select: { id: true, roles: true, email: true },
+ })
}
-export const hasRole = ({ roles }) => {
- return roles !== undefined
+/**
+ * The user is authenticated if there is a currentUser in the context
+ *
+ * @returns {boolean} - If the currentUser is authenticated
+ */
+export const isAuthenticated = (): boolean => {
+ return !!context.currentUser
+}
+
+/**
+ * When checking role membership, roles can be a single value, a list, or none.
+ * You can use Prisma enums too (if you're using them for roles), just import your enum type from `@prisma/client`
+ */
+type AllowedRoles = string | string[] | undefined
+
+/**
+ * Checks if the currentUser is authenticated (and assigned one of the given roles)
+ *
+ * @param roles: {@link AllowedRoles} - Checks if the currentUser is assigned one of these roles
+ *
+ * @returns {boolean} - Returns true if the currentUser is logged in and assigned one of the given roles,
+ * or when no roles are provided to check against. Otherwise returns false.
+ */
+export const hasRole = (roles: AllowedRoles): boolean => {
+ if (!isAuthenticated()) {
+ return false
+ }
+
+ const currentUserRoles = context.currentUser?.roles as string | string[]
+
+ if (typeof roles === 'string') {
+ if (typeof currentUserRoles === 'string') {
+ // roles to check is a string, currentUser.roles is a string
+ return currentUserRoles === roles
+ } else if (Array.isArray(currentUserRoles)) {
+ // roles to check is a string, currentUser.roles is an array
+ return currentUserRoles?.some((allowedRole) => roles === allowedRole)
+ }
+ }
+
+ if (Array.isArray(roles)) {
+ if (Array.isArray(currentUserRoles)) {
+ // roles to check is an array, currentUser.roles is an array
+ return currentUserRoles?.some((allowedRole) =>
+ roles.includes(allowedRole)
+ )
+ } else if (typeof currentUserRoles === 'string') {
+ // roles to check is an array, currentUser.roles is a string
+ return roles.some((allowedRole) => currentUserRoles === allowedRole)
+ }
+ }
+
+ // roles not found
+ return false
}
-// This is used by the redwood directive
-// in ./api/src/directives/requireAuth
+/**
+ * Use requireAuth in your services to check that a user is logged in,
+ * whether or not they are assigned a role, and optionally raise an
+ * error if they're not.
+ *
+ * @param roles: {@link AllowedRoles} - When checking role membership, these roles grant access.
+ *
+ * @returns - If the currentUser is authenticated (and assigned one of the given roles)
+ *
+ * @throws {@link AuthenticationError} - If the currentUser is not authenticated
+ * @throws {@link ForbiddenError} If the currentUser is not allowed due to role permissions
+ *
+ * @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
+ */
+export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => {
+ if (!isAuthenticated()) {
+ throw new AuthenticationError("You don't have permission to do that.")
+ }
-// Roles are passed in by the requireAuth directive if you have auth setup
-// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
-export const requireAuth = ({ roles }) => {
- return isAuthenticated()
+ if (roles && !hasRole(roles)) {
+ throw new ForbiddenError("You don't have access to do that.")
+ }
}
diff --git a/__fixtures__/fragment-test-project/api/src/lib/db.ts b/__fixtures__/fragment-test-project/api/src/lib/db.ts
index 6743f0c11d9a..5006d00aae49 100644
--- a/__fixtures__/fragment-test-project/api/src/lib/db.ts
+++ b/__fixtures__/fragment-test-project/api/src/lib/db.ts
@@ -11,11 +11,11 @@ import { logger } from './logger'
* Instance of the Prisma Client
*/
export const db = new PrismaClient({
- log: emitLogLevels(['info', 'warn', 'error', 'query']),
+ log: emitLogLevels(['info', 'warn', 'error']),
})
handlePrismaLogging({
db,
logger,
- logLevels: ['info', 'warn', 'error', 'query'],
+ logLevels: ['info', 'warn', 'error'],
})
diff --git a/__fixtures__/fragment-test-project/api/src/lib/persistedOperations.json b/__fixtures__/fragment-test-project/api/src/lib/persistedOperations.json
deleted file mode 100644
index e5a8039db67f..000000000000
--- a/__fixtures__/fragment-test-project/api/src/lib/persistedOperations.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "GetGroceries": "9198a2438e6e5dbcf42362657baca24494a9f6cf83a6033983d2a978c1c1c703",
- "GetProduce": "a8ee227d80bda6e1f785083aac537e8f1cd0340e0b52faaa27e18dbe4d629241"
-}
\ No newline at end of file
diff --git a/__fixtures__/fragment-test-project/api/src/services/contacts/contacts.scenarios.ts b/__fixtures__/fragment-test-project/api/src/services/contacts/contacts.scenarios.ts
new file mode 100644
index 000000000000..37b062292193
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/contacts/contacts.scenarios.ts
@@ -0,0 +1,12 @@
+import type { Prisma, Contact } from '@prisma/client'
+
+import type { ScenarioData } from '@redwoodjs/testing/api'
+
+export const standard = defineScenario({
+ contact: {
+ one: { data: { name: 'String', email: 'String', message: 'String' } },
+ two: { data: { name: 'String', email: 'String', message: 'String' } },
+ },
+})
+
+export type StandardScenario = ScenarioData
diff --git a/__fixtures__/fragment-test-project/api/src/services/contacts/contacts.test.ts b/__fixtures__/fragment-test-project/api/src/services/contacts/contacts.test.ts
new file mode 100644
index 000000000000..8bb328e175e9
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/contacts/contacts.test.ts
@@ -0,0 +1,59 @@
+import type { Contact } from '@prisma/client'
+
+import {
+ contacts,
+ contact,
+ createContact,
+ updateContact,
+ deleteContact,
+} from './contacts'
+import type { StandardScenario } from './contacts.scenarios'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-services
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('contacts', () => {
+ scenario('returns all contacts', async (scenario: StandardScenario) => {
+ const result = await contacts()
+
+ expect(result.length).toEqual(Object.keys(scenario.contact).length)
+ })
+
+ scenario('returns a single contact', async (scenario: StandardScenario) => {
+ const result = await contact({ id: scenario.contact.one.id })
+
+ expect(result).toEqual(scenario.contact.one)
+ })
+
+ scenario('creates a contact', async () => {
+ const result = await createContact({
+ input: { name: 'String', email: 'String', message: 'String' },
+ })
+
+ expect(result.name).toEqual('String')
+ expect(result.email).toEqual('String')
+ expect(result.message).toEqual('String')
+ })
+
+ scenario('updates a contact', async (scenario: StandardScenario) => {
+ const original = (await contact({ id: scenario.contact.one.id })) as Contact
+ const result = await updateContact({
+ id: original.id,
+ input: { name: 'String2' },
+ })
+
+ expect(result.name).toEqual('String2')
+ })
+
+ scenario('deletes a contact', async (scenario: StandardScenario) => {
+ const original = (await deleteContact({
+ id: scenario.contact.one.id,
+ })) as Contact
+ const result = await contact({ id: original.id })
+
+ expect(result).toEqual(null)
+ })
+})
diff --git a/__fixtures__/fragment-test-project/api/src/services/contacts/contacts.ts b/__fixtures__/fragment-test-project/api/src/services/contacts/contacts.ts
new file mode 100644
index 000000000000..453651e37b3c
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/contacts/contacts.ts
@@ -0,0 +1,37 @@
+import type { QueryResolvers, MutationResolvers } from 'types/graphql'
+
+import { db } from 'src/lib/db'
+
+export const contacts: QueryResolvers['contacts'] = () => {
+ return db.contact.findMany()
+}
+
+export const contact: QueryResolvers['contact'] = ({ id }) => {
+ return db.contact.findUnique({
+ where: { id },
+ })
+}
+
+export const createContact: MutationResolvers['createContact'] = ({
+ input,
+}) => {
+ return db.contact.create({
+ data: input,
+ })
+}
+
+export const updateContact: MutationResolvers['updateContact'] = ({
+ id,
+ input,
+}) => {
+ return db.contact.update({
+ data: input,
+ where: { id },
+ })
+}
+
+export const deleteContact: MutationResolvers['deleteContact'] = ({ id }) => {
+ return db.contact.delete({
+ where: { id },
+ })
+}
diff --git a/__fixtures__/fragment-test-project/api/src/services/fruit.ts b/__fixtures__/fragment-test-project/api/src/services/fruit.ts
deleted file mode 100644
index b94ff9c3796d..000000000000
--- a/__fixtures__/fragment-test-project/api/src/services/fruit.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import type { QueryResolvers } from 'types/graphql'
-
-import { db } from 'src/lib/db'
-import { logger } from 'src/lib/logger'
-
-const isFruit = { isSeedless: { not: null }, ripenessIndicators: { not: null } }
-
-export const fruits: QueryResolvers['fruits'] = async () => {
- const result = await db.produce.findMany({
- where: { ...isFruit },
- include: { stall: true },
- orderBy: { name: 'asc' },
- })
-
- logger.debug({ result }, 'frroooooots')
-
- return result
-}
-
-export const fruitById: QueryResolvers['fruitById'] = async ({ id }) => {
- const result = await db.produce.findUnique({
- where: { id, ...isFruit },
- include: { stall: true },
- })
-
- logger.debug({ result }, 'frroot')
-
- return result
-}
diff --git a/__fixtures__/fragment-test-project/api/src/services/groceries.ts b/__fixtures__/fragment-test-project/api/src/services/groceries.ts
index 8a34942d27a3..09eb5de330ff 100644
--- a/__fixtures__/fragment-test-project/api/src/services/groceries.ts
+++ b/__fixtures__/fragment-test-project/api/src/services/groceries.ts
@@ -1,6 +1,8 @@
+import { Produce } from 'types/graphql'
+
import { db } from 'src/lib/db'
-const isFruit = (grocery) => {
+const isFruit = (grocery: Produce) => {
return grocery.isSeedless !== null && grocery.ripenessIndicators !== null
}
@@ -11,7 +13,7 @@ export const groceries = async () => {
})
const avail = result.map((grocery) => {
- if (isFruit) {
+ if (isFruit(grocery)) {
return {
...grocery,
__typename: 'Fruit',
diff --git a/__fixtures__/fragment-test-project/api/src/services/posts/posts.scenarios.ts b/__fixtures__/fragment-test-project/api/src/services/posts/posts.scenarios.ts
new file mode 100644
index 000000000000..9201d41f7c03
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/posts/posts.scenarios.ts
@@ -0,0 +1,38 @@
+import type { Prisma, Post } from '@prisma/client'
+
+import type { ScenarioData } from '@redwoodjs/testing/api'
+
+export const standard = defineScenario({
+ post: {
+ one: {
+ data: {
+ title: 'String',
+ body: 'String',
+ author: {
+ create: {
+ email: 'String12',
+ hashedPassword: 'String',
+ fullName: 'String',
+ salt: 'String',
+ },
+ },
+ },
+ },
+ two: {
+ data: {
+ title: 'String',
+ body: 'String',
+ author: {
+ create: {
+ email: 'String68383',
+ hashedPassword: 'String',
+ fullName: 'String',
+ salt: 'String',
+ },
+ },
+ },
+ },
+ },
+})
+
+export type StandardScenario = ScenarioData
diff --git a/__fixtures__/fragment-test-project/api/src/services/posts/posts.test.ts b/__fixtures__/fragment-test-project/api/src/services/posts/posts.test.ts
new file mode 100644
index 000000000000..fc316574f5ab
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/posts/posts.test.ts
@@ -0,0 +1,55 @@
+import type { Post } from '@prisma/client'
+
+import { posts, post, createPost, updatePost, deletePost } from './posts'
+import type { StandardScenario } from './posts.scenarios'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-services
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('posts', () => {
+ scenario('returns all posts', async (scenario: StandardScenario) => {
+ const result = await posts()
+
+ expect(result.length).toEqual(Object.keys(scenario.post).length)
+ })
+
+ scenario('returns a single post', async (scenario: StandardScenario) => {
+ const result = await post({ id: scenario.post.one.id })
+
+ expect(result).toEqual(scenario.post.one)
+ })
+
+ scenario('creates a post', async (scenario: StandardScenario) => {
+ const result = await createPost({
+ input: {
+ title: 'String',
+ body: 'String',
+ authorId: scenario.post.two.authorId,
+ },
+ })
+
+ expect(result.title).toEqual('String')
+ expect(result.body).toEqual('String')
+ expect(result.authorId).toEqual(scenario.post.two.authorId)
+ })
+
+ scenario('updates a post', async (scenario: StandardScenario) => {
+ const original = (await post({ id: scenario.post.one.id })) as Post
+ const result = await updatePost({
+ id: original.id,
+ input: { title: 'String2' },
+ })
+
+ expect(result.title).toEqual('String2')
+ })
+
+ scenario('deletes a post', async (scenario: StandardScenario) => {
+ const original = (await deletePost({ id: scenario.post.one.id })) as Post
+ const result = await post({ id: original.id })
+
+ expect(result).toEqual(null)
+ })
+})
diff --git a/__fixtures__/fragment-test-project/api/src/services/posts/posts.ts b/__fixtures__/fragment-test-project/api/src/services/posts/posts.ts
new file mode 100644
index 000000000000..eaeff31fb74b
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/posts/posts.ts
@@ -0,0 +1,42 @@
+import type {
+ QueryResolvers,
+ MutationResolvers,
+ PostRelationResolvers,
+} from 'types/graphql'
+
+import { db } from 'src/lib/db'
+
+export const posts: QueryResolvers['posts'] = () => {
+ return db.post.findMany()
+}
+
+export const post: QueryResolvers['post'] = ({ id }) => {
+ return db.post.findUnique({
+ where: { id },
+ })
+}
+
+export const createPost: MutationResolvers['createPost'] = ({ input }) => {
+ return db.post.create({
+ data: input,
+ })
+}
+
+export const updatePost: MutationResolvers['updatePost'] = ({ id, input }) => {
+ return db.post.update({
+ data: input,
+ where: { id },
+ })
+}
+
+export const deletePost: MutationResolvers['deletePost'] = ({ id }) => {
+ return db.post.delete({
+ where: { id },
+ })
+}
+
+export const Post: PostRelationResolvers = {
+ author: (_obj, { root }) => {
+ return db.post.findUnique({ where: { id: root?.id } }).author()
+ },
+}
diff --git a/__fixtures__/fragment-test-project/api/src/services/produces/produces.scenarios.ts b/__fixtures__/fragment-test-project/api/src/services/produces/produces.scenarios.ts
new file mode 100644
index 000000000000..475e638a5d58
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/produces/produces.scenarios.ts
@@ -0,0 +1,27 @@
+import type { Prisma, Produce } from '@prisma/client'
+import type { ScenarioData } from '@redwoodjs/testing/api'
+
+export const standard = defineScenario({
+ produce: {
+ one: {
+ data: {
+ name: 'String6430168',
+ quantity: 7893718,
+ price: 1113110,
+ region: 'String',
+ stall: { create: { name: 'String', stallNumber: 'String1437797' } },
+ },
+ },
+ two: {
+ data: {
+ name: 'String2325729',
+ quantity: 9170370,
+ price: 9020391,
+ region: 'String',
+ stall: { create: { name: 'String', stallNumber: 'String8553241' } },
+ },
+ },
+ },
+})
+
+export type StandardScenario = ScenarioData
diff --git a/__fixtures__/fragment-test-project/api/src/services/produces/produces.test.ts b/__fixtures__/fragment-test-project/api/src/services/produces/produces.test.ts
new file mode 100644
index 000000000000..38e3457c1150
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/produces/produces.test.ts
@@ -0,0 +1,67 @@
+import type { Produce } from '@prisma/client'
+
+import {
+ produces,
+ produce,
+ createProduce,
+ updateProduce,
+ deleteProduce,
+} from './produces'
+import type { StandardScenario } from './produces.scenarios'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-services
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('produces', () => {
+ scenario('returns all produces', async (scenario: StandardScenario) => {
+ const result = await produces()
+
+ expect(result.length).toEqual(Object.keys(scenario.produce).length)
+ })
+
+ scenario('returns a single produce', async (scenario: StandardScenario) => {
+ const result = await produce({ id: scenario.produce.one.id })
+
+ expect(result).toEqual(scenario.produce.one)
+ })
+
+ scenario('creates a produce', async (scenario: StandardScenario) => {
+ const result = await createProduce({
+ input: {
+ name: 'String9956690',
+ quantity: 4900208,
+ price: 6716153,
+ region: 'String',
+ stallId: scenario.produce.two.stallId,
+ },
+ })
+
+ expect(result.name).toEqual('String9956690')
+ expect(result.quantity).toEqual(4900208)
+ expect(result.price).toEqual(6716153)
+ expect(result.region).toEqual('String')
+ expect(result.stallId).toEqual(scenario.produce.two.stallId)
+ })
+
+ scenario('updates a produce', async (scenario: StandardScenario) => {
+ const original = (await produce({ id: scenario.produce.one.id })) as Produce
+ const result = await updateProduce({
+ id: original.id,
+ input: { name: 'String87087152' },
+ })
+
+ expect(result.name).toEqual('String87087152')
+ })
+
+ scenario('deletes a produce', async (scenario: StandardScenario) => {
+ const original = (await deleteProduce({
+ id: scenario.produce.one.id,
+ })) as Produce
+ const result = await produce({ id: original.id })
+
+ expect(result).toEqual(null)
+ })
+})
diff --git a/__fixtures__/fragment-test-project/api/src/services/produces/produces.ts b/__fixtures__/fragment-test-project/api/src/services/produces/produces.ts
new file mode 100644
index 000000000000..5e99407a6d7b
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/produces/produces.ts
@@ -0,0 +1,47 @@
+import type {
+ QueryResolvers,
+ MutationResolvers,
+ ProduceRelationResolvers,
+} from 'types/graphql'
+
+import { db } from 'src/lib/db'
+
+export const produces: QueryResolvers['produces'] = () => {
+ return db.produce.findMany()
+}
+
+export const produce: QueryResolvers['produce'] = ({ id }) => {
+ return db.produce.findUnique({
+ where: { id },
+ })
+}
+
+export const createProduce: MutationResolvers['createProduce'] = ({
+ input,
+}) => {
+ return db.produce.create({
+ data: input,
+ })
+}
+
+export const updateProduce: MutationResolvers['updateProduce'] = ({
+ id,
+ input,
+}) => {
+ return db.produce.update({
+ data: input,
+ where: { id },
+ })
+}
+
+export const deleteProduce: MutationResolvers['deleteProduce'] = ({ id }) => {
+ return db.produce.delete({
+ where: { id },
+ })
+}
+
+export const Produce: ProduceRelationResolvers = {
+ stall: (_obj, { root }) => {
+ return db.produce.findUnique({ where: { id: root?.id } }).stall()
+ },
+}
diff --git a/__fixtures__/fragment-test-project/api/src/services/stalls.ts b/__fixtures__/fragment-test-project/api/src/services/stalls.ts
deleted file mode 100644
index aa7d417fd10c..000000000000
--- a/__fixtures__/fragment-test-project/api/src/services/stalls.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { QueryResolvers } from 'types/graphql'
-
-import { db } from 'src/lib/db'
-
-export const stalls: QueryResolvers['stalls'] = async () => {
- const result = await db.stall.findMany({
- include: { produce: true },
- orderBy: { name: 'asc' },
- })
-
- return result
-}
-
-export const stallById: QueryResolvers['stallById'] = async ({ id }) => {
- const result = await db.stall.findUnique({
- where: { id },
- include: { produce: true },
- })
-
- return result
-}
diff --git a/__fixtures__/fragment-test-project/api/src/services/stalls/stalls.scenarios.ts b/__fixtures__/fragment-test-project/api/src/services/stalls/stalls.scenarios.ts
new file mode 100644
index 000000000000..fd57beaade6e
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/stalls/stalls.scenarios.ts
@@ -0,0 +1,11 @@
+import type { Prisma, Stall } from '@prisma/client'
+import type { ScenarioData } from '@redwoodjs/testing/api'
+
+export const standard = defineScenario({
+ stall: {
+ one: { data: { name: 'String', stallNumber: 'String3227467' } },
+ two: { data: { name: 'String', stallNumber: 'String3142426' } },
+ },
+})
+
+export type StandardScenario = ScenarioData
diff --git a/__fixtures__/fragment-test-project/api/src/services/stalls/stalls.test.ts b/__fixtures__/fragment-test-project/api/src/services/stalls/stalls.test.ts
new file mode 100644
index 000000000000..e527a42691f6
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/stalls/stalls.test.ts
@@ -0,0 +1,50 @@
+import type { Stall } from '@prisma/client'
+
+import { stalls, stall, createStall, updateStall, deleteStall } from './stalls'
+import type { StandardScenario } from './stalls.scenarios'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-services
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('stalls', () => {
+ scenario('returns all stalls', async (scenario: StandardScenario) => {
+ const result = await stalls()
+
+ expect(result.length).toEqual(Object.keys(scenario.stall).length)
+ })
+
+ scenario('returns a single stall', async (scenario: StandardScenario) => {
+ const result = await stall({ id: scenario.stall.one.id })
+
+ expect(result).toEqual(scenario.stall.one)
+ })
+
+ scenario('creates a stall', async () => {
+ const result = await createStall({
+ input: { name: 'String', stallNumber: 'String7681055' },
+ })
+
+ expect(result.name).toEqual('String')
+ expect(result.stallNumber).toEqual('String7681055')
+ })
+
+ scenario('updates a stall', async (scenario: StandardScenario) => {
+ const original = (await stall({ id: scenario.stall.one.id })) as Stall
+ const result = await updateStall({
+ id: original.id,
+ input: { name: 'String2' },
+ })
+
+ expect(result.name).toEqual('String2')
+ })
+
+ scenario('deletes a stall', async (scenario: StandardScenario) => {
+ const original = (await deleteStall({ id: scenario.stall.one.id })) as Stall
+ const result = await stall({ id: original.id })
+
+ expect(result).toEqual(null)
+ })
+})
diff --git a/__fixtures__/fragment-test-project/api/src/services/stalls/stalls.ts b/__fixtures__/fragment-test-project/api/src/services/stalls/stalls.ts
new file mode 100644
index 000000000000..91c7ce00cda3
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/stalls/stalls.ts
@@ -0,0 +1,45 @@
+import type {
+ QueryResolvers,
+ MutationResolvers,
+ StallRelationResolvers,
+} from 'types/graphql'
+
+import { db } from 'src/lib/db'
+
+export const stalls: QueryResolvers['stalls'] = () => {
+ return db.stall.findMany()
+}
+
+export const stall: QueryResolvers['stall'] = ({ id }) => {
+ return db.stall.findUnique({
+ where: { id },
+ })
+}
+
+export const createStall: MutationResolvers['createStall'] = ({ input }) => {
+ return db.stall.create({
+ data: input,
+ })
+}
+
+export const updateStall: MutationResolvers['updateStall'] = ({
+ id,
+ input,
+}) => {
+ return db.stall.update({
+ data: input,
+ where: { id },
+ })
+}
+
+export const deleteStall: MutationResolvers['deleteStall'] = ({ id }) => {
+ return db.stall.delete({
+ where: { id },
+ })
+}
+
+export const Stall: StallRelationResolvers = {
+ produce: (_obj, { root }) => {
+ return db.stall.findUnique({ where: { id: root?.id } }).produce()
+ },
+}
diff --git a/__fixtures__/fragment-test-project/api/src/services/users/users.scenarios.ts b/__fixtures__/fragment-test-project/api/src/services/users/users.scenarios.ts
new file mode 100644
index 000000000000..fccdc7ae6d7e
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/users/users.scenarios.ts
@@ -0,0 +1,26 @@
+import type { Prisma, User } from '@prisma/client'
+
+import type { ScenarioData } from '@redwoodjs/testing/api'
+
+export const standard = defineScenario({
+ user: {
+ one: {
+ data: {
+ email: 'String8',
+ hashedPassword: 'String',
+ fullName: 'String',
+ salt: 'String',
+ },
+ },
+ two: {
+ data: {
+ email: 'String16',
+ hashedPassword: 'String',
+ fullName: 'String',
+ salt: 'String',
+ },
+ },
+ },
+})
+
+export type StandardScenario = ScenarioData
diff --git a/__fixtures__/fragment-test-project/api/src/services/users/users.test.ts b/__fixtures__/fragment-test-project/api/src/services/users/users.test.ts
new file mode 100644
index 000000000000..0e183cf8cc2d
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/users/users.test.ts
@@ -0,0 +1,10 @@
+import { user } from './users'
+import type { StandardScenario } from './users.scenarios'
+
+describe('users', () => {
+ scenario('returns a single user', async (scenario: StandardScenario) => {
+ const result = await user({ id: scenario.user.one.id })
+
+ expect(result).toEqual(scenario.user.one)
+ })
+})
diff --git a/__fixtures__/fragment-test-project/api/src/services/users/users.ts b/__fixtures__/fragment-test-project/api/src/services/users/users.ts
new file mode 100644
index 000000000000..1160d12f8400
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/users/users.ts
@@ -0,0 +1,17 @@
+import type { QueryResolvers, UserRelationResolvers } from 'types/graphql'
+
+import { db } from 'src/lib/db'
+
+export {}
+
+export const user: QueryResolvers['user'] = ({ id }) => {
+ return db.user.findUnique({
+ where: { id },
+ })
+}
+
+export const User: UserRelationResolvers = {
+ posts: (_obj, { root }) => {
+ return db.user.findUnique({ where: { id: root?.id } }).posts()
+ },
+}
diff --git a/__fixtures__/fragment-test-project/api/src/services/vegetables.ts b/__fixtures__/fragment-test-project/api/src/services/vegetables.ts
deleted file mode 100644
index 7caf39d47fea..000000000000
--- a/__fixtures__/fragment-test-project/api/src/services/vegetables.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import type { QueryResolvers } from 'types/graphql'
-
-import { db } from 'src/lib/db'
-
-const isVegetable = { vegetableFamily: { not: null }, isPickled: { not: null } }
-
-export const vegetables: QueryResolvers['vegetables'] = async () => {
- return await db.produce.findMany({
- where: { ...isVegetable },
- include: { stall: true },
- orderBy: { name: 'asc' },
- })
-}
-
-export const vegetableById: QueryResolvers['vegetableById'] = async ({
- id,
-}) => {
- return await db.produce.findUnique({
- where: { id, ...isVegetable },
- include: { stall: true },
- })
-}
diff --git a/__fixtures__/fragment-test-project/jest.config.js b/__fixtures__/fragment-test-project/jest.config.js
new file mode 100644
index 000000000000..c6b395cb762a
--- /dev/null
+++ b/__fixtures__/fragment-test-project/jest.config.js
@@ -0,0 +1,8 @@
+// This the Redwood root jest config
+// Each side, e.g. ./web/ and ./api/ has specific config that references this root
+// More info at https://redwoodjs.com/docs/project-configuration-dev-test-build
+
+module.exports = {
+ rootDir: '.',
+ projects: ['/{*,!(node_modules)/**/}/jest.config.js'],
+}
diff --git a/__fixtures__/fragment-test-project/package.json b/__fixtures__/fragment-test-project/package.json
new file mode 100644
index 000000000000..1ab4a33bedab
--- /dev/null
+++ b/__fixtures__/fragment-test-project/package.json
@@ -0,0 +1,23 @@
+{
+ "private": true,
+ "workspaces": {
+ "packages": [
+ "api",
+ "web"
+ ]
+ },
+ "devDependencies": {
+ "@redwoodjs/core": "6.0.7"
+ },
+ "eslintConfig": {
+ "extends": "@redwoodjs/eslint-config",
+ "root": true
+ },
+ "engines": {
+ "node": "=20.x"
+ },
+ "prisma": {
+ "seed": "yarn rw exec seed"
+ },
+ "packageManager": "yarn@4.0.2"
+}
diff --git a/__fixtures__/fragment-test-project/prettier.config.js b/__fixtures__/fragment-test-project/prettier.config.js
new file mode 100644
index 000000000000..e532314d54ac
--- /dev/null
+++ b/__fixtures__/fragment-test-project/prettier.config.js
@@ -0,0 +1,20 @@
+// https://prettier.io/docs/en/options.html
+/** @type {import('prettier').RequiredOptions} */
+module.exports = {
+ trailingComma: 'es5',
+ bracketSpacing: true,
+ tabWidth: 2,
+ semi: false,
+ singleQuote: true,
+ arrowParens: 'always',
+ overrides: [
+ {
+ files: 'Routes.*',
+ options: {
+ printWidth: 999,
+ },
+ },
+ ],
+ tailwindConfig: './web/config/tailwind.config.js',
+ plugins: [require('prettier-plugin-tailwindcss')],
+}
diff --git a/__fixtures__/fragment-test-project/redwood.toml b/__fixtures__/fragment-test-project/redwood.toml
index 32d7509275c0..e21a6085fa47 100644
--- a/__fixtures__/fragment-test-project/redwood.toml
+++ b/__fixtures__/fragment-test-project/redwood.toml
@@ -7,12 +7,18 @@
[web]
title = "Redwood App"
- port = 8910
- apiUrl = "/.redwood/functions" # you can customise graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths
- includeEnvironmentVariables = [] # any ENV vars that should be available to the web side, see https://redwoodjs.com/docs/environment-variables#web
-[graphql]
- fragments = true
+ port = "${WEB_DEV_PORT:8910}"
+ apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths
+ includeEnvironmentVariables = [
+ # Add any ENV vars that should be available to the web side to this array
+ # See https://redwoodjs.com/docs/environment-variables#web
+ ]
[api]
- port = 8911
+ port = "${API_DEV_PORT:8911}"
[browser]
open = true
+[notifications]
+ versionUpdates = ["latest"]
+
+[graphql]
+ fragments = true
diff --git a/__fixtures__/fragment-test-project/scripts/seed.ts b/__fixtures__/fragment-test-project/scripts/seed.ts
new file mode 100644
index 000000000000..899534721c78
--- /dev/null
+++ b/__fixtures__/fragment-test-project/scripts/seed.ts
@@ -0,0 +1,195 @@
+import type { Prisma } from '@prisma/client'
+import { db } from 'api/src/lib/db'
+
+export default async () => {
+ try {
+ const users = [
+ {
+ id: 1,
+ email: 'user.one@example.com',
+ hashedPassword: 'fake_hash',
+ fullName: 'User One',
+ salt: 'fake_salt',
+ },
+ {
+ id: 2,
+ email: 'user.two@example.com',
+ hashedPassword: 'fake_hash',
+ fullName: 'User Two',
+ salt: 'fake_salt',
+ },
+ ]
+
+ if ((await db.user.count()) === 0) {
+ await Promise.all(users.map((user) => db.user.create({ data: user })))
+ } else {
+ console.log('Users already seeded')
+ }
+ } catch (error) {
+ console.error(error)
+ }
+
+ try {
+ const posts = [
+ {
+ title: 'Welcome to the blog!',
+ body: "I'm baby single- origin coffee kickstarter lo - fi paleo skateboard.Tumblr hashtag austin whatever DIY plaid knausgaard fanny pack messenger bag blog next level woke.Ethical bitters fixie freegan,helvetica pitchfork 90's tbh chillwave mustache godard subway tile ramps art party. Hammock sustainable twee yr bushwick disrupt unicorn, before they sold out direct trade chicharrones etsy polaroid hoodie. Gentrify offal hoodie fingerstache.",
+ authorId: 1,
+ },
+ {
+ title: 'A little more about me',
+ body: "Raclette shoreditch before they sold out lyft. Ethical bicycle rights meh prism twee. Tote bag ennui vice, slow-carb taiyaki crucifix whatever you probably haven't heard of them jianbing raw denim DIY hot chicken. Chillwave blog succulents freegan synth af ramps poutine wayfarers yr seitan roof party squid. Jianbing flexitarian gentrify hexagon portland single-origin coffee raclette gluten-free. Coloring book cloud bread street art kitsch lumbersexual af distillery ethical ugh thundercats roof party poke chillwave. 90's palo santo green juice subway tile, prism viral butcher selvage etsy pitchfork sriracha tumeric bushwick.",
+ authorId: 1,
+ },
+ {
+ title: 'What is the meaning of life?',
+ body: 'Meh waistcoat succulents umami asymmetrical, hoodie post-ironic paleo chillwave tote bag. Trust fund kitsch waistcoat vape, cray offal gochujang food truck cloud bread enamel pin forage. Roof party chambray ugh occupy fam stumptown. Dreamcatcher tousled snackwave, typewriter lyft unicorn pabst portland blue bottle locavore squid PBR&B tattooed.',
+ authorId: 2,
+ },
+ ]
+
+ if ((await db.post.count()) === 0) {
+ await Promise.all(
+ posts.map(async (post) => {
+ const newPost = await db.post.create({ data: post })
+
+ console.log(newPost)
+ })
+ )
+ } else {
+ console.log('Posts already seeded')
+ }
+ } catch (error) {
+ console.error(error)
+ }
+
+ try {
+ const stalls = [
+ {
+ id: 'clr0zv6ow000012nvo6r09vog',
+ name: 'Salad Veggies',
+ stallNumber: '1',
+ },
+ {
+ id: 'clr0zvne2000112nvyhzf1ifk',
+ name: 'Pie Veggies',
+ stallNumber: '2',
+ },
+ {
+ id: 'clr0zvne3000212nv6boae9qw',
+ name: 'Root Veggies',
+ stallNumber: '3',
+ },
+ ]
+
+ if ((await db.stall.count()) === 0) {
+ await Promise.all(
+ stalls.map(async (stall) => {
+ const newStall = await db.stall.create({ data: stall })
+
+ console.log(newStall)
+ })
+ )
+ } else {
+ console.log('Stalls already seeded')
+ }
+
+ const produce = [
+ {
+ id: 'clr0zwyoq000312nvfsu1efcw',
+ name: 'Lettuce',
+ quantity: 10,
+ price: 2,
+ ripenessIndicators: null,
+ region: '',
+ isSeedless: false,
+ vegetableFamily: 'Asteraceae',
+ stallId: 'clr0zv6ow000012nvo6r09vog',
+ },
+ {
+ id: 'clr0zy32x000412nvsya5g8q0',
+ name: 'Strawberries',
+ quantity: 24,
+ price: 3,
+ ripenessIndicators: 'Vitamin C',
+ region: 'California',
+ isSeedless: false,
+ vegetableFamily: 'Soft',
+ stallId: 'clr0zvne2000112nvyhzf1ifk',
+ },
+ ]
+
+ if ((await db.produce.count()) === 0) {
+ await Promise.all(
+ produce.map(async (produce) => {
+ const newProduce = await db.produce.create({ data: produce })
+
+ console.log(newProduce)
+ })
+ )
+ } else {
+ console.log('Produce already seeded')
+ }
+ } catch (error) {
+ console.error(error)
+ }
+
+ try {
+ //
+ // Manually seed via `yarn rw prisma db seed`
+ // Seeds automatically with `yarn rw prisma migrate dev` and `yarn rw prisma migrate reset`
+ //
+ // Update "const data = []" to match your data model and seeding needs
+ //
+ const data: Prisma.UserExampleCreateArgs['data'][] = [
+ // To try this example data with the UserExample model in schema.prisma,
+ // uncomment the lines below and run 'yarn rw prisma migrate dev'
+ //
+ // { name: 'alice', email: 'alice@example.com' },
+ // { name: 'mark', email: 'mark@example.com' },
+ // { name: 'jackie', email: 'jackie@example.com' },
+ // { name: 'bob', email: 'bob@example.com' },
+ ]
+ console.log(
+ "\nUsing the default './scripts/seed.{js,ts}' template\nEdit the file to add seed data\n"
+ )
+
+ // Note: if using PostgreSQL, using `createMany` to insert multiple records is much faster
+ // @see: https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#createmany
+ await Promise.all(
+ //
+ // Change to match your data model and seeding needs
+ //
+ data.map(async (data: Prisma.UserExampleCreateArgs['data']) => {
+ const record = await db.userExample.create({ data })
+ console.log(record)
+ })
+ )
+
+ // If using dbAuth and seeding users, you'll need to add a `hashedPassword`
+ // and associated `salt` to their record. Here's how to create them using
+ // the same algorithm that dbAuth uses internally:
+ //
+ // import { hashPassword } from '@redwoodjs/auth-dbauth-api'
+ //
+ // const users = [
+ // { name: 'john', email: 'john@example.com', password: 'secret1' },
+ // { name: 'jane', email: 'jane@example.com', password: 'secret2' }
+ // ]
+ //
+ // for (const user of users) {
+ // const [hashedPassword, salt] = hashPassword(user.password)
+ // await db.user.create({
+ // data: {
+ // name: user.name,
+ // email: user.email,
+ // hashedPassword,
+ // salt
+ // }
+ // })
+ // }
+ } catch (error) {
+ console.warn('Please define your seed data.')
+ console.error(error)
+ }
+}
diff --git a/__fixtures__/fragment-test-project/scripts/tsconfig.json b/__fixtures__/fragment-test-project/scripts/tsconfig.json
new file mode 100644
index 000000000000..babc7c436be6
--- /dev/null
+++ b/__fixtures__/fragment-test-project/scripts/tsconfig.json
@@ -0,0 +1,42 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "allowJs": true,
+ "esModuleInterop": true,
+ "target": "esnext",
+ "module": "esnext",
+ "moduleResolution": "node",
+ "baseUrl": "./",
+ "paths": {
+ "$api/*": [
+ "../api/*"
+ ],
+ "api/*": [
+ "../api/*"
+ ],
+ "$web/*": [
+ "../web/*"
+ ],
+ "web/*": [
+ "../web/*"
+ ],
+ "$web/src/*": [
+ "../web/src/*",
+ "../.redwood/types/mirror/web/src/*"
+ ],
+ "web/src/*": [
+ "../web/src/*",
+ "../.redwood/types/mirror/web/src/*"
+ ],
+ "types/*": ["../types/*", "../web/types/*", "../api/types/*"]
+ },
+ "typeRoots": ["../node_modules/@types"],
+ "jsx": "preserve"
+ },
+ "include": [
+ ".",
+ "../.redwood/types/includes/all-*",
+ "../.redwood/types/includes/web-*",
+ "../types"
+ ]
+}
diff --git a/__fixtures__/fragment-test-project/web/config/postcss.config.js b/__fixtures__/fragment-test-project/web/config/postcss.config.js
new file mode 100644
index 000000000000..ca420cad420e
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/config/postcss.config.js
@@ -0,0 +1,8 @@
+const path = require('path')
+
+module.exports = {
+ plugins: [
+ require('tailwindcss')(path.resolve(__dirname, 'tailwind.config.js')),
+ require('autoprefixer'),
+ ],
+}
diff --git a/__fixtures__/fragment-test-project/web/config/tailwind.config.js b/__fixtures__/fragment-test-project/web/config/tailwind.config.js
new file mode 100644
index 000000000000..baf368f417d2
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/config/tailwind.config.js
@@ -0,0 +1,8 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ['src/**/*.{js,jsx,ts,tsx}'],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
diff --git a/__fixtures__/fragment-test-project/web/package.json b/__fixtures__/fragment-test-project/web/package.json
index 1bffdb2f6902..089dd533bc0c 100644
--- a/__fixtures__/fragment-test-project/web/package.json
+++ b/__fixtures__/fragment-test-project/web/package.json
@@ -11,14 +11,22 @@
]
},
"dependencies": {
- "@redwoodjs/forms": "6.2.0",
- "@redwoodjs/router": "6.2.0",
- "@redwoodjs/web": "6.2.0",
- "prop-types": "15.8.1",
- "react": "18.2.0",
- "react-dom": "18.2.0"
+ "@redwoodjs/auth-dbauth-web": "6.0.7",
+ "@redwoodjs/forms": "6.0.7",
+ "@redwoodjs/router": "6.0.7",
+ "@redwoodjs/web": "6.0.7",
+ "humanize-string": "2.1.0",
+ "react": "0.0.0-experimental-e5205658f-20230913",
+ "react-dom": "0.0.0-experimental-e5205658f-20230913"
},
"devDependencies": {
- "@redwoodjs/vite": "6.2.0"
+ "@redwoodjs/vite": "6.0.7",
+ "@types/react": "18.2.37",
+ "@types/react-dom": "18.2.15",
+ "autoprefixer": "^10.4.16",
+ "postcss": "^8.4.33",
+ "postcss-loader": "^7.3.4",
+ "prettier-plugin-tailwindcss": "0.4.1",
+ "tailwindcss": "^3.4.1"
}
}
diff --git a/__fixtures__/fragment-test-project/web/public/README.md b/__fixtures__/fragment-test-project/web/public/README.md
new file mode 100644
index 000000000000..618395f02033
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/public/README.md
@@ -0,0 +1,35 @@
+# Static Assets
+Use this folder to add static files directly to your app. All included files and folders will be copied directly into the `/dist` folder (created when Vite builds for production). They will also be available during development when you run `yarn rw dev`.
+>Note: files will *not* hot reload while the development server is running. You'll need to manually stop/start to access file changes.
+
+### Example Use
+A file like `favicon.png` will be copied to `/dist/favicon.png`. A folder containing a file such as `static-files/my-logo.jpg` will be copied to `/dist/static-files/my-logo.jpg`. These can be referenced in your code directly without any special handling, e.g.
+```
+
+```
+and
+```
+ alt="Logo" />
+```
+
+
+## Best Practices
+Because assets in this folder are bypassing the javascript module system, **this folder should be used sparingly** for assets such as favicons, robots.txt, manifests, libraries incompatible with Vite, etc.
+
+In general, it's best to import files directly into a template, page, or component. This allows Vite to include that file in the bundle when small enough, or to copy it over to the `dist` folder with a hash.
+
+### Example Asset Import with Vite
+Instead of handling our logo image as a static file per the example above, we can do the following:
+```
+import React from "react"
+import logo from "./my-logo.jpg"
+
+
+function Header() {
+ return
+}
+
+export default Header
+```
+
+See Vite's docs for [static asset handling](https://vitejs.dev/guide/assets.html)
diff --git a/__fixtures__/fragment-test-project/web/public/favicon.png b/__fixtures__/fragment-test-project/web/public/favicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..47414294173cb0795dcafb8813599fc382282556
GIT binary patch
literal 1741
zcmV;;1~U1HP)u3dvWaK1Jt7p7xtk~lm38V(vb%~9EcN4itP(!;||l>?RBBL}g^A8<`Fn
z=_ofw?w2~Qt#0f9Ac3O;;Nt1}TFWmPb1YZ9hDBXZ
zTK55jh;jRpRArCUs~@6m!BMLSuZE&5;HTqrDc^;f)?K|FaV6o1RTFbt+uA;);7z?5
z9axBZCgX!V;dhWl*HZCE&V7oz;oZ;*lOh^wZ2aYlLI<1rXkc0&HH!|5!S0|*s-
zM*~yi#Ef4dES_G+_-z+`S<%x__Ulk8{Z?I!;wv8DmN?3t1H$+fJ*q^w!}
z8`oOx{i(WL4oLgKN0~^gQyJ3t#+tnIhR=h}6@BVu1&_1g7*O6j$-5z)KLsPi3dqCH
zq+n<+)2a$Afvr|B97(#s5f6-oU6qYHP<2rWEKfC)aEc=?j9nPwEyIiT4XCI%BScNpoU1Cro6M@BSt>YU4@z^JQPbj-
zbMl0tf(CkBNTVH0run?8E#6lyouay;Bf8|_ud%WyA2Dkqc}nAEGkyiO!|#6>OX~jC
z_3u?iQ>Xm%XNGGb_3~zzqyj(lHYRC##{sV_zNQl$KP40jQHRR#WeJ!akxfaL;HU(y
z@6A7KA;pjflPx?{&_wwQ<6?f(Uld(h*XSf+Ct`QR3EDfau;y#nNiKfJ`Ny24=O+_9
z{chAh!5R0T(`<1ayxDvCtBZ?9Rn)QBoddzqchGPN4C8rB2tQ(*#m6zlySN7XwxM)X
zNo%g}Q*?B_&%_K;!PvNxj9-D>BYn6zcIb@VGE=-?gP+zjpQ4x$*@_cm*TL-MtWeV+
z%v$Vh+2e#jDJ4Yc3NPgE9Uhr~V;6)j#bgMC+5!L2yYdX5ef->+k9d_?db{`}fWW+F
zU&GKd9pW?cv0e8pA%20doi=OgaTV=dLOHx7cgAQlYDkLWaAUksGbO`Z7+>qo}~5K=?ZI!b@vaF5}r7-
zyP2aiwSn}KbwGhrQ0A?W4L_Jwg?C#vAElLzpK~}}&ny0d@_GVhUqVEfXX9}XI8%B;
z;BYTG$dM}6WS8urD4fqn$733@mNss6jB7yHY*76e*L=X6apM|Dgg^tZhpge9{Ojy9
z{Sl&x=vUbHU+7KFQEas^U*jQ8^rj_XAzI=0y_Nmx3ChT&K?_-b!N10g5+C9TqMGZ@!a>mh#`}nJM>Cu2v@32F*rQ(x05Xb64
zV-ML!u$4W31M7A@mi~3fnSOQSZ->>TC+02Mt+0csMl0*2TCklB$VOH11pW{4
zD1)V+^h4n@OYlO&;Z!-dk{(LVtA%;(o#!>jYgG>s%eL0iXx~jJsrfL3rwo;cc52kP
zRnvwZId>`-FV`PUvUKk4gU&nzX&+gTEm1bNsCdaXc
zvaOny-3X43Fs?Jn;>*U?jaR1`9KIVP?p(?ulraQZc;T0UKos^SChGJoJYVu1%?E0v
zDGNOfZKPrPKtyFYEU~bZZ~rB{4X2ko>_VJlJw3rw-!>TIT6R!3;POq5yNZdnfu$Ao
j!CVlN4fQVi0D=DiS&&%ubg+{I00000NkvXXu0mjf8bDG2
literal 0
HcmV?d00001
diff --git a/__fixtures__/fragment-test-project/web/public/robots.txt b/__fixtures__/fragment-test-project/web/public/robots.txt
new file mode 100644
index 000000000000..eb0536286f30
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/public/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow:
diff --git a/__fixtures__/fragment-test-project/web/src/App.tsx b/__fixtures__/fragment-test-project/web/src/App.tsx
index f60aa1dfc22a..65419d60c7d6 100644
--- a/__fixtures__/fragment-test-project/web/src/App.tsx
+++ b/__fixtures__/fragment-test-project/web/src/App.tsx
@@ -1,22 +1,22 @@
import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web'
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
-import introspection from 'src/graphql/possibleTypes'
import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'
+import { AuthProvider, useAuth } from './auth'
+
+import './scaffold.css'
+import './index.css'
+
const App = () => (
-
-
-
+
+
+
+
+
)
diff --git a/__fixtures__/fragment-test-project/web/src/Redwood.stories.mdx b/__fixtures__/fragment-test-project/web/src/Redwood.stories.mdx
new file mode 100644
index 000000000000..4481bff2fdc9
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/Redwood.stories.mdx
@@ -0,0 +1,15 @@
+import { Meta } from '@storybook/addon-docs'
+
+
+
+
+
+
Redwood
+
+
+_by Tom Preston-Werner, Peter Pistorius, Rob Cameron, David Price, and more than
+250 amazing contributors (see end of file for a full list)._
+
+**Redwood is an opinionated, full-stack, JavaScript/TypeScript web application
+framework designed to keep you moving fast as your app grows from side project
+to startup.**
diff --git a/__fixtures__/fragment-test-project/web/src/Routes.tsx b/__fixtures__/fragment-test-project/web/src/Routes.tsx
index 71fc2e0f7098..a8b80e306513 100644
--- a/__fixtures__/fragment-test-project/web/src/Routes.tsx
+++ b/__fixtures__/fragment-test-project/web/src/Routes.tsx
@@ -7,13 +7,46 @@
// 'src/pages/HomePage/HomePage.js' -> HomePage
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
-import { Router, Route } from '@redwoodjs/router'
+import { Router, Route, Private, Set } from '@redwoodjs/router'
+
+import BlogLayout from 'src/layouts/BlogLayout'
+import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
+import HomePage from 'src/pages/HomePage'
+
+import { useAuth } from './auth'
const Routes = () => {
return (
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/__fixtures__/fragment-test-project/web/src/auth.ts b/__fixtures__/fragment-test-project/web/src/auth.ts
new file mode 100644
index 000000000000..143e75bd61a2
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/auth.ts
@@ -0,0 +1,5 @@
+import { createDbAuthClient, createAuth } from '@redwoodjs/auth-dbauth-web'
+
+const dbAuthClient = createDbAuthClient()
+
+export const { AuthProvider, useAuth } = createAuth(dbAuthClient)
diff --git a/__fixtures__/fragment-test-project/web/src/components/Author/Author.stories.tsx b/__fixtures__/fragment-test-project/web/src/components/Author/Author.stories.tsx
new file mode 100644
index 000000000000..662b48ec9883
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/components/Author/Author.stories.tsx
@@ -0,0 +1,35 @@
+// Pass props to your component by passing an `args` object to your story
+//
+// ```tsx
+// export const Primary: Story = {
+// args: {
+// propName: propValue
+// }
+// }
+// ```
+//
+// See https://storybook.js.org/docs/react/writing-stories/args.
+
+import type { Meta, StoryObj } from '@storybook/react'
+
+import Author from './Author'
+
+const meta: Meta = {
+ component: Author,
+ tags: ['autodocs'],
+}
+
+export default meta
+
+type Story = StoryObj
+
+const author = {
+ email: 'story.user@email.com',
+ fullName: 'Story User',
+}
+
+export const Primary: Story = {
+ render: () => {
+ return
+ },
+}
diff --git a/__fixtures__/fragment-test-project/web/src/components/Author/Author.test.tsx b/__fixtures__/fragment-test-project/web/src/components/Author/Author.test.tsx
new file mode 100644
index 000000000000..9dfa8c73830f
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/components/Author/Author.test.tsx
@@ -0,0 +1,19 @@
+import { render } from '@redwoodjs/testing/web'
+
+import Author from './Author'
+
+const author = {
+ email: 'test.user@email.com',
+ fullName: 'Test User',
+}
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-components
+
+describe('Author', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render()
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/fragment-test-project/web/src/components/Author/Author.tsx b/__fixtures__/fragment-test-project/web/src/components/Author/Author.tsx
new file mode 100644
index 000000000000..f59f8b98f106
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/components/Author/Author.tsx
@@ -0,0 +1,16 @@
+interface Props {
+ author: {
+ email: string
+ fullName: string
+ }
+}
+
+const Author = ({ author }: Props) => {
+ return (
+
+ {author.fullName} ({author.email})
+
+ )
+}
+
+export default Author
diff --git a/__fixtures__/fragment-test-project/web/src/components/AuthorCell/AuthorCell.mock.ts b/__fixtures__/fragment-test-project/web/src/components/AuthorCell/AuthorCell.mock.ts
new file mode 100644
index 000000000000..4473d93d09de
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/components/AuthorCell/AuthorCell.mock.ts
@@ -0,0 +1,8 @@
+// Define your own mock data here:
+export const standard = (/* vars, { ctx, req } */) => ({
+ author: {
+ id: 42,
+ email: 'fortytwo@42.com',
+ fullName: 'Forty Two',
+ },
+})
diff --git a/__fixtures__/fragment-test-project/web/src/components/AuthorCell/AuthorCell.stories.tsx b/__fixtures__/fragment-test-project/web/src/components/AuthorCell/AuthorCell.stories.tsx
new file mode 100644
index 000000000000..fd0a2b73376b
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/components/AuthorCell/AuthorCell.stories.tsx
@@ -0,0 +1,35 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import { Loading, Empty, Failure, Success } from './AuthorCell'
+import { standard } from './AuthorCell.mock'
+
+const meta: Meta = {
+ title: 'Cells/AuthorCell',
+ tags: ['autodocs'],
+}
+
+export default meta
+
+export const loading: StoryObj = {
+ render: () => {
+ return Loading ? : <>>
+ },
+}
+
+export const empty: StoryObj = {
+ render: () => {
+ return Empty ? : <>>
+ },
+}
+
+export const failure: StoryObj = {
+ render: (args) => {
+ return Failure ? : <>>
+ },
+}
+
+export const success: StoryObj = {
+ render: (args) => {
+ return Success ? : <>>
+ },
+}
diff --git a/__fixtures__/fragment-test-project/web/src/components/AuthorCell/AuthorCell.test.tsx b/__fixtures__/fragment-test-project/web/src/components/AuthorCell/AuthorCell.test.tsx
new file mode 100644
index 000000000000..ade2ee4f44c7
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/components/AuthorCell/AuthorCell.test.tsx
@@ -0,0 +1,42 @@
+import { render } from '@redwoodjs/testing/web'
+
+import { Loading, Empty, Failure, Success } from './AuthorCell'
+import { standard } from './AuthorCell.mock'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float and DateTime types.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-cells
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('AuthorCell', () => {
+ it('renders Loading successfully', () => {
+ expect(() => {
+ render()
+ }).not.toThrow()
+ })
+
+ it('renders Empty successfully', async () => {
+ expect(() => {
+ render()
+ }).not.toThrow()
+ })
+
+ it('renders Failure successfully', async () => {
+ expect(() => {
+ render()
+ }).not.toThrow()
+ })
+
+ // When you're ready to test the actual output of your component render
+ // you could test that, for example, certain text is present:
+ //
+ // 1. import { screen } from '@redwoodjs/testing/web'
+ // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
+
+ it('renders Success successfully', async () => {
+ expect(() => {
+ render()
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/fragment-test-project/web/src/components/AuthorCell/AuthorCell.tsx b/__fixtures__/fragment-test-project/web/src/components/AuthorCell/AuthorCell.tsx
new file mode 100644
index 000000000000..f4633be36fb3
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/components/AuthorCell/AuthorCell.tsx
@@ -0,0 +1,39 @@
+import type { FindAuthorQuery, FindAuthorQueryVariables } from 'types/graphql'
+
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@redwoodjs/web'
+
+import Author from 'src/components/Author'
+
+export const QUERY: TypedDocumentNode<
+ FindAuthorQuery,
+ FindAuthorQueryVariables
+> = gql`
+ query FindAuthorQuery($id: Int!) {
+ author: user(id: $id) {
+ email
+ fullName
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Empty = () => Empty
+
+export const Failure = ({
+ error,
+}: CellFailureProps) => (
+ Error: {error?.message}
+)
+
+export const Success = ({
+ author,
+}: CellSuccessProps) => (
+
+
+
+)
diff --git a/__fixtures__/fragment-test-project/web/src/components/BlogPost/BlogPost.stories.tsx b/__fixtures__/fragment-test-project/web/src/components/BlogPost/BlogPost.stories.tsx
new file mode 100644
index 000000000000..c1d94a471350
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/components/BlogPost/BlogPost.stories.tsx
@@ -0,0 +1,26 @@
+// Pass props to your component by passing an `args` object to your story
+//
+// ```tsx
+// export const Primary: Story = {
+// args: {
+// propName: propValue
+// }
+// }
+// ```
+//
+// See https://storybook.js.org/docs/react/writing-stories/args.
+
+import type { Meta, StoryObj } from '@storybook/react'
+
+import BlogPost from './BlogPost'
+
+const meta: Meta = {
+ component: BlogPost,
+ tags: ['autodocs'],
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/fragment-test-project/web/src/components/BlogPost/BlogPost.test.tsx b/__fixtures__/fragment-test-project/web/src/components/BlogPost/BlogPost.test.tsx
new file mode 100644
index 000000000000..df026f691ddd
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/components/BlogPost/BlogPost.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@redwoodjs/testing/web'
+
+import BlogPost from './BlogPost'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-components
+
+describe('BlogPost', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render()
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/fragment-test-project/web/src/components/BlogPost/BlogPost.tsx b/__fixtures__/fragment-test-project/web/src/components/BlogPost/BlogPost.tsx
new file mode 100644
index 000000000000..380053bc4899
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/components/BlogPost/BlogPost.tsx
@@ -0,0 +1,41 @@
+import { FindBlogPostQuery } from 'types/graphql'
+
+import { Link, routes } from '@redwoodjs/router'
+
+import Author from 'src/components/Author'
+
+interface Props extends FindBlogPostQuery {}
+
+const BlogPost = ({ blogPost }: Props) => {
+ return (
+
+ {blogPost && (
+ <>
+
+