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'