diff --git a/.github/actions/detect-changes/detectChanges.mjs b/.github/actions/detect-changes/detectChanges.mjs
index 41f012e1e076..a7119100199d 100644
--- a/.github/actions/detect-changes/detectChanges.mjs
+++ b/.github/actions/detect-changes/detectChanges.mjs
@@ -5,7 +5,7 @@ import { hasCodeChanges } from './cases/code_changes.mjs'
import { rscChanged } from './cases/rsc.mjs'
import { ssrChanged } from './cases/ssr.mjs'
-const getPrNumber = (githubRef) => {
+const getPrNumber = () => {
// Example GITHUB_REF refs/pull/9544/merge
const result = /refs\/pull\/(\d+)\/merge/g.exec(process.env.GITHUB_REF)
@@ -48,7 +48,7 @@ async function getChangedFiles(page = 1, retries = 0) {
const githubToken = process.env.GITHUB_TOKEN
const url = `https://api.github.com/repos/redwoodjs/redwood/pulls/${prNumber}/files?per_page=100&page=${page}`
let resp
- let files
+ let files = []
try {
resp = await fetch(url, {
@@ -59,6 +59,12 @@ async function getChangedFiles(page = 1, retries = 0) {
},
})
+ if (!resp.ok) {
+ console.log()
+ console.error('Response not ok')
+ console.log('resp', resp)
+ }
+
const json = await resp.json()
files = json.map((file) => file.filename) || []
} catch (e) {
@@ -70,8 +76,8 @@ async function getChangedFiles(page = 1, retries = 0) {
return []
} else {
- await new Promise((resolve) => setTimeout(resolve, 3000))
- getChangedFiles(page, ++retries)
+ await new Promise((resolve) => setTimeout(resolve, 3000 * retries))
+ files = await getChangedFiles(page, ++retries)
}
}
@@ -103,8 +109,8 @@ async function main() {
if (changedFiles.length === 0) {
console.log(
- 'No changed files found. Something must have gone wrong. Fall back to ' +
- 'running all tests.'
+ 'No changed files found. Something must have gone wrong. Falling back ' +
+ 'to running all tests.'
)
core.setOutput('onlydocs', false)
core.setOutput('rsc', true)
diff --git a/packages/internal/src/__tests__/clientPreset.test.ts b/packages/internal/src/__tests__/clientPreset.test.ts
index b55bd1630f24..cf9395b6093b 100644
--- a/packages/internal/src/__tests__/clientPreset.test.ts
+++ b/packages/internal/src/__tests__/clientPreset.test.ts
@@ -40,14 +40,13 @@ describe('Generate client preset', () => {
const { clientPresetFiles } = await generateClientPreset()
- expect(clientPresetFiles).toHaveLength(6)
+ expect(clientPresetFiles).toHaveLength(5)
const expectedEndings = [
'/fragment-masking.ts',
'/index.ts',
'/gql.ts',
'/graphql.ts',
'/persisted-documents.json',
- '/types.d.ts',
]
const foundEndings = expectedEndings.filter((expectedEnding) =>
diff --git a/packages/internal/src/generate/clientPreset.ts b/packages/internal/src/generate/clientPreset.ts
index 26f9c7a052b7..4b42b708ddd9 100644
--- a/packages/internal/src/generate/clientPreset.ts
+++ b/packages/internal/src/generate/clientPreset.ts
@@ -32,27 +32,6 @@ export const generateClientPreset = async () => {
schema: getPaths().generated.schema,
documents: documentsGlob,
generates: {
- // should be graphql.d.ts
- [`${getPaths().web.base}/types/types.d.ts`]: {
- plugins: ['typescript', 'typescript-operations', 'add'],
- config: {
- enumsAsTypes: true,
- content: 'import { Prisma } from "@prisma/client"',
- placement: 'prepend',
- scalars: {
- // We need these, otherwise these scalars are mapped to any
- BigInt: 'number',
- // @Note: DateTime fields can be valid Date-strings, or the Date object in the api side. They're always strings on the web side.
- DateTime: 'string',
- Date: 'string',
- JSON: 'Prisma.JsonValue',
- JSONObject: 'Prisma.JsonObject',
- Time: 'string',
- },
- namingConvention: 'keep', // to allow camelCased query names
- omitOperationSuffix: true,
- },
- },
[`${getPaths().web.src}/graphql/`]: {
preset: 'client',
presetConfig: {
diff --git a/packages/router/src/__tests__/links.test.tsx b/packages/router/src/__tests__/links.test.tsx
index 0b364ea11cb9..929b3c68104f 100644
--- a/packages/router/src/__tests__/links.test.tsx
+++ b/packages/router/src/__tests__/links.test.tsx
@@ -2,9 +2,8 @@ import React from 'react'
import { render } from '@testing-library/react'
-import { NavLink, useMatch, Link } from '../links'
+import { NavLink } from '../links'
import { LocationProvider } from '../location'
-import { flattenSearchParams } from '../util'
function createDummyLocation(pathname: string, search = '') {
return {
@@ -279,77 +278,3 @@ describe('', () => {
expect(getByText(/Dunder Mifflin/)).not.toHaveClass('activeTest')
})
})
-
-describe('useMatch', () => {
- const MyLink = ({
- to,
- ...rest
- }: React.ComponentPropsWithoutRef) => {
- const [pathname, queryString] = to.split('?')
- const matchInfo = useMatch(pathname, {
- searchParams: flattenSearchParams(queryString),
- })
-
- return (
-
- )
- }
-
- it('returns a match on the same pathname', () => {
- const mockLocation = createDummyLocation('/dunder-mifflin')
-
- const { getByText } = render(
-
- Dunder Mifflin
-
- )
-
- expect(getByText(/Dunder Mifflin/)).toHaveStyle('color: green')
- })
-
- it('returns a match on the same pathname with search parameters', () => {
- const mockLocation = createDummyLocation(
- '/search-params',
- '?page=1&tab=main'
- )
-
- const { getByText } = render(
-
- Dunder Mifflin
-
- )
-
- expect(getByText(/Dunder Mifflin/)).toHaveStyle('color: green')
- })
-
- it('does NOT receive active class on different path', () => {
- const mockLocation = createDummyLocation('/staples')
-
- const { getByText } = render(
-
- Dunder Mifflin
-
- )
-
- expect(getByText(/Dunder Mifflin/)).toHaveStyle('color: red')
- })
-
- it('does NOT receive active class on the same pathname with different parameters', () => {
- const mockLocation = createDummyLocation(
- '/search-params',
- '?tab=main&page=1'
- )
-
- const { getByText } = render(
-
- Dunder Mifflin
-
- )
-
- expect(getByText(/Dunder Mifflin/)).toHaveStyle('color: red')
- })
-})
diff --git a/packages/router/src/__tests__/useMatch.test.tsx b/packages/router/src/__tests__/useMatch.test.tsx
new file mode 100644
index 000000000000..89d7bcdd8419
--- /dev/null
+++ b/packages/router/src/__tests__/useMatch.test.tsx
@@ -0,0 +1,100 @@
+import React from 'react'
+
+import { render } from '@testing-library/react'
+
+import { Link } from '../links'
+import { LocationProvider } from '../location'
+import { useMatch } from '../useMatch'
+import { flattenSearchParams } from '../util'
+
+function createDummyLocation(pathname: string, search = '') {
+ return {
+ pathname,
+ hash: '',
+ host: '',
+ hostname: '',
+ href: '',
+ ancestorOrigins: null,
+ assign: () => null,
+ reload: () => null,
+ replace: () => null,
+ origin: '',
+ port: '',
+ protocol: '',
+ search,
+ }
+}
+
+describe('useMatch', () => {
+ const MyLink = ({
+ to,
+ ...rest
+ }: React.ComponentPropsWithoutRef) => {
+ const [pathname, queryString] = to.split('?')
+ const matchInfo = useMatch(pathname, {
+ searchParams: flattenSearchParams(queryString),
+ })
+
+ return (
+
+ )
+ }
+
+ it('returns a match on the same pathname', () => {
+ const mockLocation = createDummyLocation('/dunder-mifflin')
+
+ const { getByText } = render(
+
+ Dunder Mifflin
+
+ )
+
+ expect(getByText(/Dunder Mifflin/)).toHaveStyle('color: green')
+ })
+
+ it('returns a match on the same pathname with search parameters', () => {
+ const mockLocation = createDummyLocation(
+ '/search-params',
+ '?page=1&tab=main'
+ )
+
+ const { getByText } = render(
+
+ Dunder Mifflin
+
+ )
+
+ expect(getByText(/Dunder Mifflin/)).toHaveStyle('color: green')
+ })
+
+ it('does NOT receive active class on different path', () => {
+ const mockLocation = createDummyLocation('/staples')
+
+ const { getByText } = render(
+
+ Dunder Mifflin
+
+ )
+
+ expect(getByText(/Dunder Mifflin/)).toHaveStyle('color: red')
+ })
+
+ it('does NOT receive active class on the same pathname with different parameters', () => {
+ const mockLocation = createDummyLocation(
+ '/search-params',
+ '?tab=main&page=1'
+ )
+
+ const { getByText } = render(
+
+ Dunder Mifflin
+
+ )
+
+ expect(getByText(/Dunder Mifflin/)).toHaveStyle('color: red')
+ })
+})
diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts
index 816fd3afc715..56c715a8bbec 100644
--- a/packages/router/src/index.ts
+++ b/packages/router/src/index.ts
@@ -3,7 +3,7 @@
// latter of which has closely inspired some of this code).
export { navigate, back } from './history'
-export { Link, NavLink, useMatch, Redirect } from './links'
+export { Link, NavLink, Redirect } from './links'
export { useLocation, LocationProvider } from './location'
export {
usePageLoadingContext,
@@ -20,6 +20,7 @@ export { default as RouteFocus } from './route-focus'
export * from './route-focus'
export * from './useRouteName'
export * from './useRoutePaths'
+export * from './useMatch'
export { parseSearch, getRouteRegexAndParams, matchPath } from './util'
diff --git a/packages/router/src/links.tsx b/packages/router/src/links.tsx
index 3dcde9ef0864..aea10f978d6a 100644
--- a/packages/router/src/links.tsx
+++ b/packages/router/src/links.tsx
@@ -2,67 +2,9 @@ import { forwardRef, useEffect } from 'react'
import type { NavigateOptions } from './history'
import { navigate } from './history'
-import { useLocation } from './location'
-import { flattenSearchParams, matchPath } from './util'
-
-type FlattenSearchParams = ReturnType
-type UseMatchOptions = {
- searchParams?: FlattenSearchParams
- matchSubPaths?: boolean
-}
-
-/**
- * Returns an object of { match: boolean; params: Record; }
- * 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 searchParams options to match the current location.search
- *
- * This is useful for components that need to know "active" state, e.g.
- * .
- *
- * Examples:
- *
- * Match search params key existence
- * const match = useMatch('/about', { searchParams: ['category', 'page'] })
- *
- * Match search params key and value
- * const match = useMatch('/items', { searchParams: [{page: 2}, {category: 'book'}] })
- *
- * Mix match
- * const match = useMatch('/list', { searchParams: [{page: 2}, 'gtm'] })
- *
- * Match sub paths
- * const match = useMatch('/product', { matchSubPaths: true })
- *
- */
-const useMatch = (pathname: string, options?: UseMatchOptions) => {
- const location = useLocation()
- if (!location) {
- return { match: false }
- }
-
- if (options?.searchParams) {
- const locationParams = new URLSearchParams(location.search)
- const hasUnmatched = options.searchParams.some((param) => {
- if (typeof param === 'string') {
- return !locationParams.has(param)
- } else {
- return Object.keys(param).some(
- (key) => param[key] != locationParams.get(key)
- )
- }
- })
-
- if (hasUnmatched) {
- return { match: false }
- }
- }
-
- return matchPath(pathname, location.pathname, {
- matchSubPaths: options?.matchSubPaths,
- })
-}
+import { useMatch } from './useMatch'
+import type { FlattenSearchParams } from './util'
+import { flattenSearchParams } from './util'
interface LinkProps {
to: string
@@ -187,4 +129,4 @@ const Redirect = ({ to, options }: RedirectProps) => {
return null
}
-export { Link, NavLink, useMatch, Redirect }
+export { Link, NavLink, Redirect }
diff --git a/packages/router/src/useMatch.ts b/packages/router/src/useMatch.ts
new file mode 100644
index 000000000000..180e7a93e094
--- /dev/null
+++ b/packages/router/src/useMatch.ts
@@ -0,0 +1,60 @@
+import { useLocation } from './location'
+import { matchPath } from './util'
+import type { FlattenSearchParams } from './util'
+
+type UseMatchOptions = {
+ searchParams?: FlattenSearchParams
+ matchSubPaths?: boolean
+}
+
+/**
+ * Returns an object of { match: boolean; params: Record; }
+ * 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 searchParams options to match the current location.search
+ *
+ * This is useful for components that need to know "active" state, e.g.
+ * .
+ *
+ * Examples:
+ *
+ * Match search params key existence
+ * const match = useMatch('/about', { searchParams: ['category', 'page'] })
+ *
+ * Match search params key and value
+ * const match = useMatch('/items', { searchParams: [{page: 2}, {category: 'book'}] })
+ *
+ * Mix match
+ * const match = useMatch('/list', { searchParams: [{page: 2}, 'gtm'] })
+ *
+ * Match sub paths
+ * const match = useMatch('/product', { matchSubPaths: true })
+ */
+export const useMatch = (pathname: string, options?: UseMatchOptions) => {
+ const location = useLocation()
+ if (!location) {
+ return { match: false }
+ }
+
+ if (options?.searchParams) {
+ const locationParams = new URLSearchParams(location.search)
+ const hasUnmatched = options.searchParams.some((param) => {
+ if (typeof param === 'string') {
+ return !locationParams.has(param)
+ } else {
+ return Object.keys(param).some(
+ (key) => param[key] != locationParams.get(key)
+ )
+ }
+ })
+
+ if (hasUnmatched) {
+ return { match: false }
+ }
+ }
+
+ return matchPath(pathname, location.pathname, {
+ matchSubPaths: options?.matchSubPaths,
+ })
+}
diff --git a/packages/router/src/useRouteName.tsx b/packages/router/src/useRouteName.ts
similarity index 100%
rename from packages/router/src/useRouteName.tsx
rename to packages/router/src/useRouteName.ts
diff --git a/packages/router/src/util.ts b/packages/router/src/util.ts
index 1b42eb05ca51..27b358c9ecd6 100644
--- a/packages/router/src/util.ts
+++ b/packages/router/src/util.ts
@@ -154,7 +154,7 @@ export function matchPath(
// Map extracted values to their param name, casting the value if needed
const providedParams = matches[0].slice(1)
- // @NOTE: refers to definiton e.g. '/page/{id}', not the actual params
+ // @NOTE: refers to definition e.g. '/page/{id}', not the actual params
if (routeParamsDefinition.length > 0) {
const params = providedParams.reduce>(
(acc, value, index) => {
@@ -348,8 +348,9 @@ export function replaceParams(
return path
}
+export type FlattenSearchParams = ReturnType
+
/**
- *
* @param {string} queryString
* @returns {Array>} A flat array of search params
*
@@ -362,7 +363,6 @@ export function replaceParams(
*
* flattenSearchParams(parseSearch('?key1=val1&key2=val2'))
* => [ { key1: 'val1' }, { key2: 'val2' } ]
- *
*/
export function flattenSearchParams(
queryString: string
diff --git a/packages/web/src/components/GraphQLHooksProvider.tsx b/packages/web/src/components/GraphQLHooksProvider.tsx
index 45dfc5ff6643..d0f35bd868b8 100644
--- a/packages/web/src/components/GraphQLHooksProvider.tsx
+++ b/packages/web/src/components/GraphQLHooksProvider.tsx
@@ -3,8 +3,11 @@ import type {
useBackgroundQuery as apolloUseBackgroundQuery,
useReadQuery as apolloUseReadQuery,
} from '@apollo/client'
+import type { TypedDocumentNode } from '@graphql-typed-document-node/core'
import type { DocumentNode } from 'graphql'
+export type { TypedDocumentNode }
+
/**
* @NOTE
* The types QueryOperationResult, MutationOperationResult, SubscriptionOperationResult, and SuspenseQueryOperationResult
@@ -19,7 +22,7 @@ type DefaultUseQueryType = <
TData = any,
TVariables extends OperationVariables = GraphQLOperationVariables
>(
- query: DocumentNode,
+ query: DocumentNode | TypedDocumentNode,
options?: GraphQLQueryHookOptions
) => QueryOperationResult
@@ -27,7 +30,7 @@ type DefaultUseMutationType = <
TData = any,
TVariables = GraphQLOperationVariables
>(
- mutation: DocumentNode,
+ mutation: DocumentNode | TypedDocumentNode,
options?: GraphQLMutationHookOptions
) => MutationOperationResult
@@ -35,7 +38,7 @@ type DefaultUseSubscriptionType = <
TData = any,
TVariables extends OperationVariables = GraphQLOperationVariables
>(
- subscription: DocumentNode,
+ subscription: DocumentNode | TypedDocumentNode,
options?: GraphQLSubscriptionHookOptions
) => SubscriptionOperationResult
@@ -43,7 +46,7 @@ type DefaultUseSuspenseType = <
TData = any,
TVariables extends OperationVariables = GraphQLOperationVariables
>(
- query: DocumentNode,
+ query: DocumentNode | TypedDocumentNode,
options?: GraphQLSuspenseQueryHookOptions
) => SuspenseQueryOperationResult
@@ -152,7 +155,7 @@ export function useQuery<
TData = any,
TVariables extends OperationVariables = GraphQLOperationVariables
>(
- query: DocumentNode,
+ query: DocumentNode | TypedDocumentNode,
options?: GraphQLQueryHookOptions
): QueryOperationResult {
return React.useContext(GraphQLHooksContext).useQuery(
@@ -165,7 +168,7 @@ export function useMutation<
TData = any,
TVariables = GraphQLOperationVariables
>(
- mutation: DocumentNode,
+ mutation: DocumentNode | TypedDocumentNode,
options?: GraphQLMutationHookOptions
): MutationOperationResult {
return React.useContext(GraphQLHooksContext).useMutation(
@@ -178,7 +181,7 @@ export function useSubscription<
TData = any,
TVariables extends OperationVariables = GraphQLOperationVariables
>(
- query: DocumentNode,
+ query: DocumentNode | TypedDocumentNode,
options?: GraphQLSubscriptionHookOptions
): SubscriptionOperationResult {
return React.useContext(GraphQLHooksContext).useSubscription<
@@ -191,7 +194,7 @@ export function useSuspenseQuery<
TData = any,
TVariables extends OperationVariables = GraphQLOperationVariables
>(
- query: DocumentNode,
+ query: DocumentNode | TypedDocumentNode,
options?: GraphQLSuspenseQueryHookOptions
): SuspenseQueryOperationResult {
return React.useContext(GraphQLHooksContext).useSuspenseQuery<
diff --git a/packages/web/src/index.ts b/packages/web/src/index.ts
index 908c04485573..90c870aefb9a 100644
--- a/packages/web/src/index.ts
+++ b/packages/web/src/index.ts
@@ -38,3 +38,5 @@ export * from './components/htmlTags'
export * from './routeHooks.types'
export * from './components/ServerInject'
+
+export type { TypedDocumentNode } from './components/GraphQLHooksProvider'