From c7aa4291d0890831dfce4c4f8e2bbf4e88664ac8 Mon Sep 17 00:00:00 2001 From: esteban <14810250+esteban-url@users.noreply.github.com> Date: Wed, 1 Mar 2023 19:35:09 -0600 Subject: [PATCH] fix: avoid naming conflict with `client` prop (#7024) * fix: avoid naming conflict with `client` prop * fix: disable lint error for prefer-const * refactor: group variables under queryResult * fix: add type safety for handling errorCode * refactor: deconstruct props directly * Codemod for cell parameter adjustments --------- Co-authored-by: Josh-Walker-GM <56300765+Josh-Walker-GM@users.noreply.github.com> --- .../codemods/v5.x.x/cellQueryResult/README.md | 22 ++ .../__testfixtures__/client.input.tsx | 33 +++ .../__testfixtures__/client.output.tsx | 34 +++ .../__testfixtures__/default.input.ts | 32 +++ .../__testfixtures__/default.output.ts | 32 +++ .../__testfixtures__/failureSuccess.input.tsx | 36 +++ .../failureSuccess.output.tsx | 36 +++ .../__testfixtures__/refetch.input.tsx | 33 +++ .../__testfixtures__/refetch.output.tsx | 34 +++ .../__testfixtures__/refetchClient.input.tsx | 34 +++ .../__testfixtures__/refetchClient.output.tsx | 34 +++ .../refetchClientAliased.input.tsx | 34 +++ .../refetchClientAliased.output.tsx | 34 +++ .../__tests__/cellQueryResult.test.ts | 25 ++ .../v5.x.x/cellQueryResult/cellQueryResult.ts | 115 ++++++++ .../cellQueryResult/cellQueryResult.yargs.ts | 24 ++ packages/codemods/src/lib/cells.ts | 258 ++++++++++++++++++ packages/web/src/components/createCell.tsx | 33 ++- 18 files changed, 874 insertions(+), 9 deletions(-) create mode 100644 packages/codemods/src/codemods/v5.x.x/cellQueryResult/README.md create mode 100644 packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/client.input.tsx create mode 100644 packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/client.output.tsx create mode 100644 packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/default.input.ts create mode 100644 packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/default.output.ts create mode 100644 packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/failureSuccess.input.tsx create mode 100644 packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/failureSuccess.output.tsx create mode 100644 packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetch.input.tsx create mode 100644 packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetch.output.tsx create mode 100644 packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetchClient.input.tsx create mode 100644 packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetchClient.output.tsx create mode 100644 packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetchClientAliased.input.tsx create mode 100644 packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetchClientAliased.output.tsx create mode 100644 packages/codemods/src/codemods/v5.x.x/cellQueryResult/__tests__/cellQueryResult.test.ts create mode 100644 packages/codemods/src/codemods/v5.x.x/cellQueryResult/cellQueryResult.ts create mode 100644 packages/codemods/src/codemods/v5.x.x/cellQueryResult/cellQueryResult.yargs.ts create mode 100644 packages/codemods/src/lib/cells.ts diff --git a/packages/codemods/src/codemods/v5.x.x/cellQueryResult/README.md b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/README.md new file mode 100644 index 000000000000..b66ac35b227e --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/README.md @@ -0,0 +1,22 @@ +# Cell QueryResult + +RedwoodJS v5 no longer spreads the additional properties returned by a query result when passing properties into a cell. + +Prior to v5, Redwood would spread properties such as `client` or `refetch` which were returned by the query result for use within the first parameter of a cell. See https://www.apollographql.com/docs/react/data/queries/#result for the properties returned by Apollo. This would in some cases restrict the choice of naming the query result as the data would be overridden by these additional query result properties. In v5 these additional properties (all but `loading`, `error` and `data`) are passed via a `queryResult` property. + +Prior to v5 access to the additional query result properties was possible like so: +```ts +export const Success = ({ model, client: apolloClient, refetch }: CellSuccessProps) => { + // Access to apolloClient or refetch is possible + return +} +``` + +This codemod removes any occurrence of these previously spread variables from the parameters and instead provides them via the destructuring of `queryResult`. This results in: + +```ts +export const Success = ({ model, queryResult: {client: apolloClient, refetch} }: CellSuccessProps) => { + // Access to apolloClient or refetch is still possible via the nested destructuring + return +} +``` diff --git a/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/client.input.tsx b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/client.input.tsx new file mode 100644 index 000000000000..1433ef039aaa --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/client.input.tsx @@ -0,0 +1,33 @@ +import type { FindAuthorQuery, FindAuthorQueryVariables } from 'types/graphql' + +import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' + +import Author from 'src/components/Author' + +export const QUERY = 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, + client +}: CellSuccessProps) => ( + + + +) diff --git a/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/client.output.tsx b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/client.output.tsx new file mode 100644 index 000000000000..f267f7f2f6fc --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/client.output.tsx @@ -0,0 +1,34 @@ +import type { FindAuthorQuery, FindAuthorQueryVariables } from 'types/graphql' + +import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' + +import Author from 'src/components/Author' + +export const QUERY = 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, + + queryResult: { client } +}: CellSuccessProps) => ( + + + +) diff --git a/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/default.input.ts b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/default.input.ts new file mode 100644 index 000000000000..22109b8d894d --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/default.input.ts @@ -0,0 +1,32 @@ +import type { FindAuthorQuery, FindAuthorQueryVariables } from 'types/graphql' + +import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' + +import Author from 'src/components/Author' + +export const QUERY = 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/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/default.output.ts b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/default.output.ts new file mode 100644 index 000000000000..22109b8d894d --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/default.output.ts @@ -0,0 +1,32 @@ +import type { FindAuthorQuery, FindAuthorQueryVariables } from 'types/graphql' + +import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' + +import Author from 'src/components/Author' + +export const QUERY = 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/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/failureSuccess.input.tsx b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/failureSuccess.input.tsx new file mode 100644 index 000000000000..b8a818b945fb --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/failureSuccess.input.tsx @@ -0,0 +1,36 @@ +import type { FindAuthorQuery, FindAuthorQueryVariables } from 'types/graphql' + +import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' + +import Author from 'src/components/Author' + +export const QUERY = gql` + query FindAuthorQuery($id: Int!) { + author: user(id: $id) { + email + fullName + } + } +` + +export const Loading = () => Loading... + +export const Empty = () => Empty + +export const Failure = ({ + error, + refetch: rerunQuery, + client +}: CellFailureProps) => ( + Error: {error?.message} {rerunQuery} {client} +) + +export const Success = ({ + author, + refetch, + client: apolloClient +}: CellSuccessProps) => ( + + + +) diff --git a/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/failureSuccess.output.tsx b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/failureSuccess.output.tsx new file mode 100644 index 000000000000..34de8a039c77 --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/failureSuccess.output.tsx @@ -0,0 +1,36 @@ +import type { FindAuthorQuery, FindAuthorQueryVariables } from 'types/graphql' + +import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' + +import Author from 'src/components/Author' + +export const QUERY = gql` + query FindAuthorQuery($id: Int!) { + author: user(id: $id) { + email + fullName + } + } +` + +export const Loading = () => Loading... + +export const Empty = () => Empty + +export const Failure = ({ + error, + + queryResult: {refetch: rerunQuery, client} +}: CellFailureProps) => ( + Error: {error?.message} {rerunQuery} {client} +) + +export const Success = ({ + author, + + queryResult: {refetch, client: apolloClient} +}: CellSuccessProps) => ( + + + +) diff --git a/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetch.input.tsx b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetch.input.tsx new file mode 100644 index 000000000000..016579752a6f --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetch.input.tsx @@ -0,0 +1,33 @@ +import type { FindAuthorQuery, FindAuthorQueryVariables } from 'types/graphql' + +import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' + +import Author from 'src/components/Author' + +export const QUERY = 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, + refetch +}: CellSuccessProps) => ( + + + +) diff --git a/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetch.output.tsx b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetch.output.tsx new file mode 100644 index 000000000000..dfbdc9f201ee --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetch.output.tsx @@ -0,0 +1,34 @@ +import type { FindAuthorQuery, FindAuthorQueryVariables } from 'types/graphql' + +import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' + +import Author from 'src/components/Author' + +export const QUERY = 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, + + queryResult: {refetch} +}: CellSuccessProps) => ( + + + +) diff --git a/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetchClient.input.tsx b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetchClient.input.tsx new file mode 100644 index 000000000000..731cff3cf5bf --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetchClient.input.tsx @@ -0,0 +1,34 @@ +import type { FindAuthorQuery, FindAuthorQueryVariables } from 'types/graphql' + +import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' + +import Author from 'src/components/Author' + +export const QUERY = 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, + refetch, + client +}: CellSuccessProps) => ( + + + +) diff --git a/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetchClient.output.tsx b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetchClient.output.tsx new file mode 100644 index 000000000000..9262c53bb752 --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetchClient.output.tsx @@ -0,0 +1,34 @@ +import type { FindAuthorQuery, FindAuthorQueryVariables } from 'types/graphql' + +import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' + +import Author from 'src/components/Author' + +export const QUERY = 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, + + queryResult: {refetch, client} +}: CellSuccessProps) => ( + + + +) diff --git a/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetchClientAliased.input.tsx b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetchClientAliased.input.tsx new file mode 100644 index 000000000000..b1e8e5c937f9 --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetchClientAliased.input.tsx @@ -0,0 +1,34 @@ +import type { FindAuthorQuery, FindAuthorQueryVariables } from 'types/graphql' + +import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' + +import Author from 'src/components/Author' + +export const QUERY = 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, + refetch, + client: apolloClient +}: CellSuccessProps) => ( + + + +) diff --git a/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetchClientAliased.output.tsx b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetchClientAliased.output.tsx new file mode 100644 index 000000000000..a857747b0c06 --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__testfixtures__/refetchClientAliased.output.tsx @@ -0,0 +1,34 @@ +import type { FindAuthorQuery, FindAuthorQueryVariables } from 'types/graphql' + +import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' + +import Author from 'src/components/Author' + +export const QUERY = 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, + + queryResult: {refetch, client: apolloClient} +}: CellSuccessProps) => ( + + + +) diff --git a/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__tests__/cellQueryResult.test.ts b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__tests__/cellQueryResult.test.ts new file mode 100644 index 000000000000..83d4fc493847 --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/__tests__/cellQueryResult.test.ts @@ -0,0 +1,25 @@ +describe('cellQueryResult', () => { + test('No query result properties used', async () => { + await matchTransformSnapshot('cellQueryResult', 'default') + }) + + test('Refetch alone is used', async () => { + await matchTransformSnapshot('cellQueryResult', 'refetch') + }) + + test('Client alone is used', async () => { + await matchTransformSnapshot('cellQueryResult', 'client') + }) + + test('Refetch and client are used', async () => { + await matchTransformSnapshot('cellQueryResult', 'refetchClient') + }) + + test('Refetch and client are used with client aliased', async () => { + await matchTransformSnapshot('cellQueryResult', 'refetchClientAliased') + }) + + test('Usage in Failure and Success', async () => { + await matchTransformSnapshot('cellQueryResult', 'failureSuccess') + }) +}) diff --git a/packages/codemods/src/codemods/v5.x.x/cellQueryResult/cellQueryResult.ts b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/cellQueryResult.ts new file mode 100644 index 000000000000..6617398e60ef --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/cellQueryResult.ts @@ -0,0 +1,115 @@ +import type { FileInfo, API, Property } from 'jscodeshift' + +// We need to check all of the cell functions +const cellFunctionsToCheck = ['Success', 'Failure', 'Loading', 'Empty'] + +// The list of properties which are no longer being spread +// See https://www.apollographql.com/docs/react/data/queries/#result for apollo query result properties +const nonSpreadVariables = [ + 'previousData', + 'variables', + 'networkStatus', + 'client', + 'called', + 'refetch', + 'fetchMore', + 'startPolling', + 'stopPolling', + 'subscribeToMore', + 'updateQuery', +] + +export default function transform(file: FileInfo, api: API) { + const j = api.jscodeshift + const ast = j(file.source) + + cellFunctionsToCheck.forEach((variableName) => { + const foundCellFunctions = ast.findVariableDeclarators(variableName) + if (foundCellFunctions.size() === 1) { + const foundFunction = foundCellFunctions.get() + + // We expect the variable to be a function (standard or arrow) + if ( + foundFunction.value.init.type === 'ArrowFunctionExpression' || + foundFunction.value.init.type === 'FunctionExpression' + ) { + const firstParameter = foundFunction.value.init.params.at(0) + + // No parameters taken by the function + if (!firstParameter) { + // Do nothing... + } else { + // We expect the function to be destructuring the properties the cell is passed + if (firstParameter.type === 'ObjectPattern') { + const previouslySpreadPropertiesInUse = + firstParameter.properties.filter((property: Property) => { + if (property.key.type !== 'Identifier') { + throw new Error( + 'Unable to process a parameter within the cell function' + ) + } + return nonSpreadVariables.includes(property.key.name) + }) + if (previouslySpreadPropertiesInUse.length > 0) { + // Add the newly destructured properties as function parameters + firstParameter.properties.push( + j.property( + 'init', + j.identifier('queryResult'), // Previously spead properties are now found within 'queryResult' + j.objectPattern( + // For every previously spead property in use add a destructuring + previouslySpreadPropertiesInUse.map( + (usedProperty: Property) => { + if ( + usedProperty.key.type !== 'Identifier' || + usedProperty.value.type !== 'Identifier' + ) { + throw new Error( + 'Unable to process a parameter within the cell function' + ) + } + const prop = j.property( + 'init', + j.identifier(usedProperty.key.name), + j.identifier(usedProperty.value.name) + ) + // Use an alias if one was previously defined by the user + prop.shorthand = usedProperty.shorthand + return prop + } + ) + ) + ) + ) + // Remove the existing function parameters corresponding to previously spread variables + firstParameter.properties = firstParameter.properties.filter( + (property: Property) => { + if (property.key.type !== 'Identifier') { + throw new Error('Unable to process a parameter') + } + return !nonSpreadVariables.includes(property.key.name) + } + ) + } + } else { + console.warn( + `The first parameter to '${variableName}' was not an object and we could not process this.` + ) + } + } + } else { + console.warn( + `'${variableName}' is not a function and we could not process this.` + ) + } + } else { + console.warn(`Could not find a unique '${variableName}' variable`) + } + }) + + return ast.toSource({ + trailingComma: true, + quote: 'single', + lineTerminator: '\n', + }) +} diff --git a/packages/codemods/src/codemods/v5.x.x/cellQueryResult/cellQueryResult.yargs.ts b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/cellQueryResult.yargs.ts new file mode 100644 index 000000000000..3d0a4068f56d --- /dev/null +++ b/packages/codemods/src/codemods/v5.x.x/cellQueryResult/cellQueryResult.yargs.ts @@ -0,0 +1,24 @@ +import path from 'path' + +import task from 'tasuku' + +import { findCells } from 'src/lib/cells' + +import runTransform from '../../../lib/runTransform' + +export const command = 'cell-query-result' +export const description = + '(v4.x.x->v5.x.x) Updates cells to use the queryResult property' + +export const handler = () => { + task('cellQueryResult', async ({ setOutput }) => { + await runTransform({ + transformPath: path.join(__dirname, 'cellQueryResult.ts'), + targetPaths: findCells(), + }) + + setOutput( + 'Updates to your cells are complete! Please run `yarn rw lint --fix` to prettify your code' + ) + }) +} diff --git a/packages/codemods/src/lib/cells.ts b/packages/codemods/src/lib/cells.ts new file mode 100644 index 000000000000..a1da8652bbc7 --- /dev/null +++ b/packages/codemods/src/lib/cells.ts @@ -0,0 +1,258 @@ +import fs from 'fs' +import path from 'path' + +import { types } from '@babel/core' +import { parse as babelParse, ParserPlugin } from '@babel/parser' +import traverse from '@babel/traverse' +import fg from 'fast-glob' +import { + DocumentNode, + FieldNode, + InlineFragmentNode, + OperationDefinitionNode, + OperationTypeNode, + parse, + visit, +} from 'graphql' + +import getRWPaths from './getRWPaths' + +export const findCells = (cwd: string = getRWPaths().web.src) => { + const modules = fg.sync('**/*Cell.{js,jsx,ts,tsx}', { + cwd, + absolute: true, + ignore: ['node_modules'], + }) + return modules.filter(isCellFile) +} + +export const isCellFile = (p: string) => { + const { dir, name } = path.parse(p) + + // If the path isn't on the web side it cannot be a cell + if (!isFileInsideFolder(p, getRWPaths().web.src)) { + return false + } + + // A Cell must be a directory named module. + if (!dir.endsWith(name)) { + return false + } + + const ast = fileToAst(p) + + // A Cell should not have a default export. + if (hasDefaultExport(ast)) { + return false + } + + // A Cell must export QUERY and Success. + const exports = getNamedExports(ast) + const exportedQUERY = exports.findIndex((v) => v.name === 'QUERY') !== -1 + const exportedSuccess = exports.findIndex((v) => v.name === 'Success') !== -1 + if (!exportedQUERY && !exportedSuccess) { + return false + } + + return true +} + +export const isFileInsideFolder = (filePath: string, folderPath: string) => { + const { dir } = path.parse(filePath) + const relativePathFromFolder = path.relative(folderPath, dir) + if ( + !relativePathFromFolder || + relativePathFromFolder.startsWith('..') || + path.isAbsolute(relativePathFromFolder) + ) { + return false + } else { + return true + } +} + +export const hasDefaultExport = (ast: types.Node): boolean => { + let exported = false + traverse(ast, { + ExportDefaultDeclaration() { + exported = true + return + }, + }) + return exported +} + +interface NamedExports { + name: string + type: 're-export' | 'variable' | 'function' | 'class' +} + +export const getNamedExports = (ast: types.Node): NamedExports[] => { + const namedExports: NamedExports[] = [] + traverse(ast, { + ExportNamedDeclaration(path) { + // Re-exports from other modules + // Eg: export { a, b } from './module' + const specifiers = path.node?.specifiers + if (specifiers.length) { + for (const s of specifiers) { + const id = s.exported as types.Identifier + namedExports.push({ + name: id.name, + type: 're-export', + }) + } + return + } + + const declaration = path.node.declaration + if (!declaration) { + return + } + + if (declaration.type === 'VariableDeclaration') { + const id = declaration.declarations[0].id as types.Identifier + namedExports.push({ + name: id.name as string, + type: 'variable', + }) + } else if (declaration.type === 'FunctionDeclaration') { + namedExports.push({ + name: declaration?.id?.name as string, + type: 'function', + }) + } else if (declaration.type === 'ClassDeclaration') { + namedExports.push({ + name: declaration?.id?.name, + type: 'class', + }) + } + }, + }) + + return namedExports +} + +export const fileToAst = (filePath: string): types.Node => { + const code = fs.readFileSync(filePath, 'utf-8') + + // use jsx plugin for web files, because in JS, the .jsx extension is not used + const isJsxFile = + path.extname(filePath).match(/[jt]sx$/) || + isFileInsideFolder(filePath, getRWPaths().web.base) + + const plugins = [ + 'typescript', + 'nullishCoalescingOperator', + 'objectRestSpread', + isJsxFile && 'jsx', + ].filter(Boolean) as ParserPlugin[] + + try { + return babelParse(code, { + sourceType: 'module', + plugins, + }) + } catch (e: any) { + // console.error(chalk.red(`Error parsing: ${filePath}`)) + console.error(e) + throw new Error(e?.message) // we throw, so typescript doesn't complain about returning + } +} + +export const getCellGqlQuery = (ast: types.Node) => { + let cellQuery: string | undefined = undefined + traverse(ast, { + ExportNamedDeclaration({ node }) { + if ( + node.exportKind === 'value' && + types.isVariableDeclaration(node.declaration) + ) { + const exportedQueryNode = node.declaration.declarations.find((d) => { + return ( + types.isIdentifier(d.id) && + d.id.name === 'QUERY' && + types.isTaggedTemplateExpression(d.init) + ) + }) + + if (exportedQueryNode) { + const templateExpression = + exportedQueryNode.init as types.TaggedTemplateExpression + + cellQuery = templateExpression.quasi.quasis[0].value.raw + } + } + return + }, + }) + + return cellQuery +} + +export const parseGqlQueryToAst = (gqlQuery: string) => { + const ast = parse(gqlQuery) + return parseDocumentAST(ast) +} + +export const parseDocumentAST = (document: DocumentNode) => { + const operations: Array = [] + + visit(document, { + OperationDefinition(node: OperationDefinitionNode) { + const fields: any[] = [] + + node.selectionSet.selections.forEach((field) => { + fields.push(getFields(field as FieldNode)) + }) + + operations.push({ + operation: node.operation, + name: node.name?.value, + fields, + }) + }, + }) + + return operations +} + +interface Operation { + operation: OperationTypeNode + name: string | undefined + fields: Array +} + +interface Field { + string: Array +} + +const getFields = (field: FieldNode): any => { + // base + if (!field.selectionSet) { + return field.name.value + } else { + const obj: Record = { + [field.name.value]: [], + } + + const lookAtFieldNode = (node: FieldNode | InlineFragmentNode): void => { + node.selectionSet?.selections.forEach((subField) => { + switch (subField.kind) { + case 'Field': + obj[field.name.value].push(getFields(subField as FieldNode)) + break + case 'FragmentSpread': + // TODO: Maybe this will also be needed, right now it's accounted for to not crash in the tests + break + case 'InlineFragment': + lookAtFieldNode(subField) + } + }) + } + + lookAtFieldNode(field) + + return obj + } +} diff --git a/packages/web/src/components/createCell.tsx b/packages/web/src/components/createCell.tsx index 5eef7e051b0a..608fa6e6ce17 100644 --- a/packages/web/src/components/createCell.tsx +++ b/packages/web/src/components/createCell.tsx @@ -18,7 +18,7 @@ import { useQuery } from './GraphQLHooksProvider' * * If the Cell does not have a `beforeQuery` function, then the variables are required. * - * Note that a query that doesnt take any variables is defined as {[x: string]: never} + * Note that a query that doesn't take any variables is defined as {[x: string]: never} * The ternary at the end makes sure we don't include it, otherwise it won't allow merging any * other custom props from the Success component. * @@ -293,8 +293,13 @@ export function createCell< // queryRest includes `variables: { ... }`, with any variables returned // from beforeQuery - // eslint-disable-next-line prefer-const - let { error, loading, data, ...queryRest } = useQuery(query, options) + let { + // eslint-disable-next-line prefer-const + error, + loading, + data, + ...queryResult + } = useQuery(query, options) if (globalThis.__REDWOOD__PRERENDERING) { // __REDWOOD__PRERENDERING will always either be set, or not set. So @@ -331,7 +336,7 @@ export function createCell< // All of the gql client's props aren't available when pre-rendering, // so using `any` here - queryRest = { variables } as any + queryResult = { variables } as any } else { queryCache[cacheKey] || (queryCache[cacheKey] = { @@ -345,13 +350,23 @@ export function createCell< if (error) { if (Failure) { + // errorCode is not part of the type returned by useQuery + // but it is returned as part of the queryResult + type QueryResultWithErrorCode = typeof queryResult & { + errorCode: string + } + return ( ) } else { @@ -366,7 +381,7 @@ export function createCell< {...props} {...afterQueryData} updating={loading} - {...queryRest} + queryResult={queryResult} /> ) } else { @@ -375,12 +390,12 @@ export function createCell< {...props} {...afterQueryData} updating={loading} - {...queryRest} + queryResult={queryResult} /> ) } } else if (loading) { - return + return } else { /** * There really shouldn't be an `else` here, but like any piece of software, GraphQL clients have bugs.