diff --git a/examples/using-contentful/package.json b/examples/using-contentful/package.json index f1a66f4698077..1dff4a8fba838 100644 --- a/examples/using-contentful/package.json +++ b/examples/using-contentful/package.json @@ -5,13 +5,13 @@ "version": "1.0.0", "author": "Marcus Ericsson (mericsson.com)", "dependencies": { - "gatsby": "next", - "gatsby-core-utils": "next", - "gatsby-plugin-image": "next", - "gatsby-plugin-sharp": "next", - "gatsby-plugin-typography": "next", - "gatsby-source-contentful": "next", - "gatsby-transformer-remark": "next", + "gatsby": "^5.13.0-next.1", + "gatsby-core-utils": "^4.13.0-next.0", + "gatsby-plugin-image": "^3.13.0-next.0", + "gatsby-plugin-sharp": "^5.13.0-next.0", + "gatsby-plugin-typography": "^5.13.0-next.0", + "gatsby-source-contentful": "^8.13.0-next.1", + "gatsby-transformer-remark": "^6.13.0-next.0", "prop-types": "^15.7.2", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -24,9 +24,6 @@ ], "license": "MIT", "main": "n/a", - "resolutions": { - "contentful": "6.1.3" - }, "scripts": { "develop": "gatsby develop", "build": "gatsby build", diff --git a/examples/using-contentful/src/layouts/index.js b/examples/using-contentful/src/layouts/index.js index d8dfe39dd2a82..4608bb54abe5b 100644 --- a/examples/using-contentful/src/layouts/index.js +++ b/examples/using-contentful/src/layouts/index.js @@ -12,7 +12,7 @@ const DefaultLayout = ({ children }) => ( <>

Products

@@ -60,19 +60,21 @@ CategoryTemplate.propTypes = propTypes export default CategoryTemplate export const pageQuery = graphql` - query($id: String!) { - contentfulCategory(id: { eq: $id }) { + query ($id: String!) { + contentfulContentTypeCategory(id: { eq: $id }) { title { - title + raw } icon { gatsbyImageData(layout: FIXED, width: 75) } - product { - gatsbyPath(filePath: "/products/{ContentfulProduct.id}") - id - productName { - productName + linkedFrom { + ContentfulContentTypeProduct { + gatsbyPath(filePath: "/products/{ContentfulContentTypeProduct.id}") + id + productName { + raw + } } } } diff --git a/examples/using-contentful/src/pages/image-api.js b/examples/using-contentful/src/pages/image-api.js index fb1104abe76ad..2641241d03335 100644 --- a/examples/using-contentful/src/pages/image-api.js +++ b/examples/using-contentful/src/pages/image-api.js @@ -316,8 +316,8 @@ const ImageAPI = props => { export default ImageAPI export const pageQuery = graphql` - query { - allContentfulAsset(filter: { node_locale: { eq: "en-US" } }) { + { + allContentfulAsset(filter: { sys: { locale: { eq: "en-US" } } }) { edges { node { title diff --git a/examples/using-contentful/src/pages/index.js b/examples/using-contentful/src/pages/index.js index 3f9ef900e998c..e4c4fc9966c2c 100644 --- a/examples/using-contentful/src/pages/index.js +++ b/examples/using-contentful/src/pages/index.js @@ -85,14 +85,16 @@ IndexPage.propTypes = propTypes export default IndexPage export const pageQuery = graphql` - query { - us: allContentfulProduct(filter: { node_locale: { eq: "en-US" } }) { + { + us: allContentfulContentTypeProduct( + filter: { sys: { locale: { eq: "en-US" } } } + ) { edges { node { id - gatsbyPath(filePath: "/products/{ContentfulProduct.id}") + gatsbyPath(filePath: "/products/{ContentfulContentTypeProduct.id}") productName { - productName + raw } image { gatsbyImageData(layout: FIXED, width: 75) @@ -100,13 +102,15 @@ export const pageQuery = graphql` } } } - german: allContentfulProduct(filter: { node_locale: { eq: "de" } }) { + german: allContentfulContentTypeProduct( + filter: { sys: { locale: { eq: "de" } } } + ) { edges { node { id - gatsbyPath(filePath: "/products/{ContentfulProduct.id}") + gatsbyPath(filePath: "/products/{ContentfulContentTypeProduct.id}") productName { - productName + raw } image { gatsbyImageData(layout: FIXED, width: 75) diff --git a/examples/using-contentful/src/pages/products/{ContentfulProduct.id}.js b/examples/using-contentful/src/pages/products/{ContentfulContentTypeProduct.id}.js similarity index 82% rename from examples/using-contentful/src/pages/products/{ContentfulProduct.id}.js rename to examples/using-contentful/src/pages/products/{ContentfulContentTypeProduct.id}.js index 14f574714916d..8a46b44cad269 100644 --- a/examples/using-contentful/src/pages/products/{ContentfulProduct.id}.js +++ b/examples/using-contentful/src/pages/products/{ContentfulContentTypeProduct.id}.js @@ -13,9 +13,9 @@ const propTypes = { class ProductTemplate extends React.Component { render() { - const product = this.props.data.contentfulProduct + const product = this.props.data.contentfulContentTypeProduct const { - productName: { productName }, + productName: { raw: productName }, productDescription, price, image, @@ -39,7 +39,7 @@ class ProductTemplate extends React.Component { )}

{productName}

-

Made by {brand.companyName.companyName}

+

Made by {brand.companyName.raw}

Price: ${price}
(
  • - {category.title.title} + {category.title.raw}
  • ))} @@ -70,10 +70,10 @@ ProductTemplate.propTypes = propTypes export default ProductTemplate export const pageQuery = graphql` - query($id: String!) { - contentfulProduct(id: { eq: $id }) { + query ($id: String!) { + contentfulContentTypeProduct(id: { eq: $id }) { productName { - productName + raw } productDescription { childMarkdownRemark { @@ -86,14 +86,14 @@ export const pageQuery = graphql` } brand { companyName { - companyName + raw } } categories { id - gatsbyPath(filePath: "/categories/{ContentfulCategory.id}") + gatsbyPath(filePath: "/categories/{ContentfulContentTypeCategory.id}") title { - title + raw } } } diff --git a/package.json b/package.json index a858da92766a5..47a6d4485ef3d 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,8 @@ "watch": "lerna run watch --no-sort --stream --concurrency 999" }, "workspaces": [ - "packages/*" + "packages/*", + "examples/using-contentful" ], "resolutions": { "@babel/plugin-transform-modules-commonjs": "7.18.6" diff --git a/packages/gatsby-codemods/src/bin/cli.js b/packages/gatsby-codemods/src/bin/cli.js index 4ca2827d31e89..ff851282408cc 100644 --- a/packages/gatsby-codemods/src/bin/cli.js +++ b/packages/gatsby-codemods/src/bin/cli.js @@ -2,6 +2,7 @@ import path from "path" import execa from "execa" const codemods = [ + `gatsby-source-contentful`, `gatsby-plugin-image`, `global-graphql-calls`, `import-link`, diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types-typescript.input.ts b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types-typescript.input.ts new file mode 100644 index 0000000000000..d06d64244e704 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types-typescript.input.ts @@ -0,0 +1,3 @@ +interface Data { + allContentfulTemplatePage: FooConnection +} \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types-typescript.output.ts b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types-typescript.output.ts new file mode 100644 index 0000000000000..cf15eeef36404 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types-typescript.output.ts @@ -0,0 +1,3 @@ +interface Data { + allContentfulContentTypeTemplatePage: FooConnection +} \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types.input.js new file mode 100644 index 0000000000000..c4e29d055ab67 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types.input.js @@ -0,0 +1,10 @@ +const demo = [ + ...data.allContentfulFoo.nodes, + ...data.allContentfulBar.nodes, +] +const content = data.contentfulPage.content +const { + data: { + allContentfulTemplatePage: { nodes: templatePages }, + }, +} = await graphql(``) \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types.output.js new file mode 100644 index 0000000000000..97d0afc1e4848 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/content-types.output.js @@ -0,0 +1,10 @@ +const demo = [ + ...data.allContentfulContentTypeFoo.nodes, + ...data.allContentfulContentTypeBar.nodes, +] +const content = data.contentfulContentTypePage.content +const { + data: { + allContentfulContentTypeTemplatePage: { nodes: templatePages }, + }, +} = await graphql(``) \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-asset.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-asset.input.js new file mode 100644 index 0000000000000..34b8bac2c5301 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-asset.input.js @@ -0,0 +1,7 @@ +import React from "react" + +export default () => ( + +) \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-asset.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-asset.output.js new file mode 100644 index 0000000000000..330c083d93f19 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-asset.output.js @@ -0,0 +1,7 @@ +import React from "react" + +export default () => ( + +) \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-sys.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-sys.input.js new file mode 100644 index 0000000000000..e6e4e4696953a --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-sys.input.js @@ -0,0 +1,11 @@ +const res1 = allContentfulPage.nodes.contentful_id +const res2 = allContentfulPage.nodes.sys.contentType.__typename +const { contentful_id, createdAt, updatedAt } = allContentfulPage.nodes +const { title, metaDescription, metaImage, content } = data.contentfulPage +const { foo } = result.data.allContentfulPage.nodes[0] +const { + revision, + sys: { + contentType: { __typename }, + }, +} = allContentfulPage.nodes diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-sys.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-sys.output.js new file mode 100644 index 0000000000000..3162c4b43f8b1 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/contentful-sys.output.js @@ -0,0 +1,17 @@ +const res1 = allContentfulPage.nodes.sys.id +const res2 = allContentfulPage.nodes.sys.contentType.name +const { + sys: { + id: contentful_id, + firstPublishedAt: createdAt, + publishedAt: updatedAt + } +} = allContentfulPage.nodes +const { title, metaDescription, metaImage, content } = data.contentfulContentTypePage +const { foo } = result.data.allContentfulContentTypePage.nodes[0] +const { + sys: { + publishedVersion: revision, + contentType: { name } + } +} = allContentfulPage.nodes diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/gatsby-node.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/gatsby-node.input.js new file mode 100644 index 0000000000000..0c603f87e12d8 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/gatsby-node.input.js @@ -0,0 +1,14 @@ +exports.createSchemaCustomization = null +export const createSchemaCustomization = null + +// export function createResolvers(actions) { +// actions.createResolvers({ +// ContentfulFoo: {}, +// }) +// } + +export const createResolvers = (actions) => { + actions.createResolvers({ + ContentfulFoo: {}, + }) +} \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/gatsby-node.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/gatsby-node.output.js new file mode 100644 index 0000000000000..b4b44cc19fab2 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/gatsby-node.output.js @@ -0,0 +1,14 @@ +exports.createSchemaCustomization = null +export const createSchemaCustomization = null + +// export function createResolvers(actions) { +// actions.createResolvers({ +// ContentfulFoo: {}, +// }) +// } + +export const createResolvers = (actions) => { + actions.createResolvers({ + ContentfulContentTypeFoo: {}, + }) +} \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-all.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-all.input.js new file mode 100644 index 0000000000000..c66bf5eaf0890 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-all.input.js @@ -0,0 +1,9 @@ +const result = await graphql` + { + allContentfulPage(limit: 1000) { + nodes { + id + } + } + } +` diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-all.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-all.output.js new file mode 100644 index 0000000000000..10a770abaddc4 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-all.output.js @@ -0,0 +1,7 @@ +const result = await graphql`{ + allContentfulContentTypePage(limit: 1000) { + nodes { + id + } + } +}` diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-fragment.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-fragment.input.js new file mode 100644 index 0000000000000..a368562c6f3ca --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-fragment.input.js @@ -0,0 +1,34 @@ +export const ExampleFragment = graphql` + fragment Example on ContentfulExample { + title + contentful_id + logo { + file { + url + fileName + contentType + details { + size + image { + width + height + } + } + } + } + } + { + allContentfulFoo { + nodes { + ... on ContentfulExample { + contentful_id + logo { + file { + url + } + } + } + } + } + } +` \ No newline at end of file diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-fragment.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-fragment.output.js new file mode 100644 index 0000000000000..2fedb08a9113a --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-fragment.output.js @@ -0,0 +1,29 @@ +export const ExampleFragment = graphql`fragment Example on ContentfulContentTypeExample { + title + sys { + id + } + logo { + url + fileName + contentType + size + width + height + } +} + +{ + allContentfulContentTypeFoo { + nodes { + ... on ContentfulContentTypeExample { + sys { + id + } + logo { + url + } + } + } + } +}` diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-single.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-single.input.js new file mode 100644 index 0000000000000..6f24e1c68328e --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-single.input.js @@ -0,0 +1,7 @@ +const result = await graphql(` + { + contentfulPage { + id + } + } +`) diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-single.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-single.output.js new file mode 100644 index 0000000000000..799e641992d3e --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-content-type-single.output.js @@ -0,0 +1,5 @@ +const result = await graphql(`{ + contentfulContentTypePage { + id + } +}`) diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-assets.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-assets.input.js new file mode 100644 index 0000000000000..248f80f3c80e5 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-assets.input.js @@ -0,0 +1,37 @@ +const result = await graphql(` + { + allContentfulPage( + filter: { logo: { file: { url: { ne: null } } } }, + sort: [{createdAt: ASC}, {logo: {file: {fileName: ASC}}}] + ) { + nodes { + id + logo { + file { + url + fileName + contentType + details { + size + image { + width + height + } + } + } + } + } + } + allContentfulAsset( + filter: {file: { url: { ne: null } }}, + sort: [{createdAt: ASC}, {file: {fileName: ASC}}] + ) { + nodes { + id + } + } + contentfulAsset(file: { url: { ne: null } }) { + id + } + } +`) diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-assets.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-assets.output.js new file mode 100644 index 0000000000000..98f4790628c6e --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-assets.output.js @@ -0,0 +1,29 @@ +const result = await graphql(`{ + allContentfulContentTypePage( + filter: {logo: {url: {ne: null}}} + sort: [{sys: {firstPublishedAt: ASC}}, {logo: {fileName: ASC}}] + ) { + nodes { + id + logo { + url + fileName + contentType + size + width + height + } + } + } + allContentfulAsset( + filter: {url: {ne: null}} + sort: [{sys: {firstPublishedAt: ASC}}, {fileName: ASC}] + ) { + nodes { + id + } + } + contentfulAsset(url: {ne: null}) { + id + } +}`) diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-metadata.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-metadata.input.js new file mode 100644 index 0000000000000..a946df7fd715a --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-metadata.input.js @@ -0,0 +1,29 @@ +const result = await graphql(` + { + allContentfulTag(sort: { fields: contentful_id }) { + nodes { + name + contentful_id + } + } + allContentfulNumber( + sort: { fields: contentful_id } + filter: { + metadata: { + tags: { elemMatch: { contentful_id: { eq: "numberInteger" } } } + } + } + ) { + nodes { + title + integer + metadata { + tags { + name + contentful_id + } + } + } + } + } +`) diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-metadata.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-metadata.output.js new file mode 100644 index 0000000000000..e9e6e1b952b4c --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-metadata.output.js @@ -0,0 +1,23 @@ +const result = await graphql(`{ + allContentfulTag(sort: {fields: contentful_id}) { + nodes { + name + contentful_id + } + } + allContentfulContentTypeNumber( + sort: {fields: contentful_id} + filter: {contentfulMetadata: {tags: {elemMatch: {contentful_id: {eq: "numberInteger"}}}}} + ) { + nodes { + title + integer + contentfulMetadata { + tags { + name + contentful_id + } + } + } + } +}`) diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-sys.input.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-sys.input.js new file mode 100644 index 0000000000000..6a2dbc9d3103e --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-sys.input.js @@ -0,0 +1,45 @@ +const result = await graphql(` + { + allContentfulPage(limit: 1000) { + nodes { + contentful_id + customName: node_locale + createdAt + updatedAt + revision + spaceId + sys { + type + contentType { + __typename + } + } + } + } + contentfulPage { + contentful_id + node_locale + createdAt + updatedAt + revision + spaceId + sys { + type + contentType { + __typename + } + } + } + allContentfulPage( + filter: { slug: { eq: "blog" }, node_locale: { eq: $locale } } + sort: { updatedAt: DESC } + ) { + nodes { + id + } + } + contentfulPage(slug: { eq: "blog" }, node_locale: { eq: $locale }) { + id + } + } +`) diff --git a/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-sys.output.js b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-sys.output.js new file mode 100644 index 0000000000000..a12f1e1e4998c --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__testfixtures__/gatsby-source-contentful/graphql-contentful-sys.output.js @@ -0,0 +1,43 @@ +const result = await graphql(`{ + allContentfulContentTypePage(limit: 1000) { + nodes { + sys { + id + customName: locale + firstPublishedAt + publishedAt + publishedVersion + spaceId + type + contentType { + name + } + } + } + } + contentfulContentTypePage { + sys { + id + locale + firstPublishedAt + publishedAt + publishedVersion + spaceId + type + contentType { + name + } + } + } + allContentfulContentTypePage( + filter: {slug: {eq: "blog"}, sys: {locale: {eq: $locale}}} + sort: {sys: {publishedAt: DESC}} + ) { + nodes { + id + } + } + contentfulContentTypePage(slug: {eq: "blog"}, sys: {locale: {eq: $locale}}) { + id + } +}`) diff --git a/packages/gatsby-codemods/src/transforms/__tests__/gatsby-source-contentful.js b/packages/gatsby-codemods/src/transforms/__tests__/gatsby-source-contentful.js new file mode 100644 index 0000000000000..f9a7c86eb4d56 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/__tests__/gatsby-source-contentful.js @@ -0,0 +1,27 @@ +const tests = [ + `content-types`, + `content-types-typescript`, + `contentful-asset`, + `contentful-sys`, + `gatsby-node`, + `graphql-content-type-all`, + `graphql-content-type-fragment`, + `graphql-content-type-single`, + `graphql-contentful-assets`, + `graphql-contentful-metadata`, + `graphql-contentful-sys`, +] + +const defineTest = require(`jscodeshift/dist/testUtils`).defineTest + +describe(`codemods`, () => { + tests.forEach(test => + defineTest( + __dirname, + `gatsby-source-contentful`, + null, + `gatsby-source-contentful/${test}`, + { parser: test.indexOf(`typescript`) !== -1 ? `ts` : `js` } + ) + ) +}) diff --git a/packages/gatsby-codemods/src/transforms/gatsby-source-contentful.js b/packages/gatsby-codemods/src/transforms/gatsby-source-contentful.js new file mode 100644 index 0000000000000..9e77eeba1b580 --- /dev/null +++ b/packages/gatsby-codemods/src/transforms/gatsby-source-contentful.js @@ -0,0 +1,734 @@ +import * as graphql from "graphql" +import { parse, print } from "recast" +import { transformFromAstSync, parseSync } from "@babel/core" +import { cloneDeep } from "lodash" + +export default function jsCodeShift(file) { + if ( + file.path.includes(`node_modules`) || + file.path.includes(`.cache`) || + file.path.includes(`public`) + ) { + return file.source + } + const transformedSource = babelRecast(file.source, file.path) + return transformedSource +} + +export function babelRecast(code, filePath) { + const transformedAst = parse(code, { + parser: { + parse: source => runParseSync(source, filePath), + }, + }) + + const changedTracker = { hasChanged: false, filename: filePath } // recast adds extra semicolons that mess with diffs and we want to avoid them + + const options = { + cloneInputAst: false, + code: false, + ast: true, + plugins: [[updateImport, changedTracker]], + } + + const { ast } = transformFromAstSync(transformedAst, code, options) + + if (changedTracker.hasChanged) { + return print(ast, { lineTerminator: `\n` }).code + } + return code +} + +const CONTENT_TYPE_SELECTOR_REGEX = /^(allContentful|[cC]ontentful)([A-Z0-9].+)/ +const CONTENT_TYPE_SELECTOR_BLACKLIST = [`Asset`, `Reference`, `Id`, `Tag`] +const SYS_FIELDS_TRANSFORMS = new Map([ + [`node_locale`, `locale`], + [`contentful_id`, `id`], + [`spaceId`, `spaceId`], + [`createdAt`, `firstPublishedAt`], + [`updatedAt`, `publishedAt`], + [`revision`, `publishedVersion`], +]) + +const isContentTypeSelector = selector => { + if (!selector) { + return false + } + const res = selector.match(CONTENT_TYPE_SELECTOR_REGEX) + return res && !CONTENT_TYPE_SELECTOR_BLACKLIST.includes(res[2]) +} +const updateContentfulSelector = selector => + selector.replace(`ontentful`, `ontentfulContentType`) + +const renderFilename = (path, state) => + `${state.opts.filename} (Line ${path.node.loc.start.line})` + +const injectNewFields = (selections, newFields, fieldToReplace) => { + if (!fieldToReplace) { + return [...selections, ...newFields] + } + + const fieldIndex = selections.findIndex( + ({ name }) => name?.value === fieldToReplace + ) + + return [ + ...selections.slice(0, fieldIndex), + ...newFields, + ...selections.slice(fieldIndex + 1), + ] +} + +function getNestedMemberExpressionProperties(node, t) { + const properties = [] + let current = node + while (t.isMemberExpression(current)) { + if (t.isIdentifier(current.property)) { + properties.push(current.property.name) + } + current = current.object + } + return properties.reverse() +} + +export function updateImport(babel) { + const { types: t } = babel + // Stack to keep track of nesting + const stack = [] + // Flag to indicate whether we are inside createResolvers function + let insideCreateResolvers = false + return { + visitor: { + Identifier(path, state) { + if ( + path.node.name === `createSchemaCustomization` && + state.opts.filename.match(/gatsby-node/) + ) { + console.log( + `${renderFilename( + path, + state + )}: Check your custom schema customizations if you patch or adjust schema related to Contentful. You probably can remove it now.` + ) + } + + // Rename content type identifiers within createResolvers + if (insideCreateResolvers) { + const variableName = path.node.name + + // Check if the variable name matches the regex + if (isContentTypeSelector(variableName)) { + path.node.name = updateContentfulSelector(variableName) + state.opts.hasChanged = true + } + } + }, + ObjectPattern: { + enter(path) { + // Push this ObjectPattern onto the stack as we enter it + stack.push(path.node.properties.map(prop => prop.key?.name)) + }, + exit(path, state) { + // Check if the variable name matches the regex + path.node.properties.forEach(prop => { + if (isContentTypeSelector(prop.key?.name)) { + prop.key.name = updateContentfulSelector(prop.key.name) + state.opts.hasChanged = true + } + }) + + // Rename contentType.__typename to contentType.name + if ( + JSON.stringify([[`sys`], [`contentType`], [`__typename`]]) === + JSON.stringify(stack) + ) { + const typenameProp = path.node.properties.find( + prop => prop.key.name === `__typename` + ) + if (typenameProp) { + typenameProp.key = t.identifier(`name`) + typenameProp.value = t.identifier(`name`) + } + // Merge old sys fields into new structure + } else { + const transformedSysProperties = [] + path.node.properties.forEach(property => { + if (SYS_FIELDS_TRANSFORMS.has(property.key?.name)) { + const transformedProp = { + ...property, + key: { + ...property.key, + name: SYS_FIELDS_TRANSFORMS.get(property.key.name), + }, + } + transformedSysProperties.push(transformedProp) + } + }) + if (transformedSysProperties.length) { + const sysField = { + type: `Property`, + key: { + type: `Identifier`, + name: `sys`, + }, + value: { + type: `ObjectPattern`, + properties: transformedSysProperties, + }, + } + path.node.properties = injectSysField( + sysField, + path.node.properties + ) + + state.opts.hasChanged = true + } + } + + // Pop this ObjectPattern off the stack as we exit it + stack.pop() + }, + }, + MemberExpression(path, state) { + const nestedProperties = getNestedMemberExpressionProperties( + path.node, + t + ) + + const assetFlatStructure = new Map([ + [`url`, [`url`, `file`]], + [`fileName`, [`fileName`, `file`]], + [`contentType`, [`contentType`, `file`]], + [`size`, [`size`, `details`, `file`]], + [`width`, [`width`, `image`, `details`, `file`]], + [`height`, [`height`, `image`, `details`, `file`]], + ]) + + for (const [newProp, oldProps] of assetFlatStructure) { + if ( + nestedProperties.slice(-oldProps.length).join(`.`) === + oldProps.reverse().join(`.`) + ) { + // We found a matching nested property. + // Rebuild the MemberExpression with the new structure. + let baseExpression = path.node + for (let i = 0; i < oldProps.length; i++) { + baseExpression = baseExpression.object + } + const newExpression = t.memberExpression( + baseExpression, + t.identifier(newProp) + ) + path.replaceWith(newExpression) + state.opts.hasChanged = true + return + } + } + + // Identify MemberExpression for `allContentfulPage.nodes.contentfulId` + const propName = path.node.property.name + const replacement = SYS_FIELDS_TRANSFORMS.get(propName) + + if (replacement) { + // Rewrite the MemberExpression with the new property + path.node.property = t.identifier(replacement) + + // Also rewrite the parent node to `.sys.` if it's not already + if (path.node.object.property?.name !== `sys`) { + path.node.object = t.memberExpression( + path.node.object, + t.identifier(`sys`) + ) + } + state.opts.hasChanged = true + } + + // Rename sys.contentType.__typename to sys.contentType.name + if ( + propName === `__typename` && + t.isMemberExpression(path.node.object) && + t.isIdentifier(path.node.object.property, { name: `contentType` }) && + t.isMemberExpression(path.node.object.object) && + t.isIdentifier(path.node.object.object.property, { name: `sys` }) + ) { + path.node.property = t.identifier(`name`) + return + } + + if (isContentTypeSelector(path.node.property?.name)) { + if ( + path.node.object?.name === `data` || + path.node.object.property?.name === `data` + ) { + path.node.property.name = updateContentfulSelector( + path.node.property.name + ) + state.opts.hasChanged = true + } else { + console.log( + `${renderFilename(path, state)}: You might need to change "${ + path.node.property?.name + }" to "${updateContentfulSelector(path.node.property.name)}"` + ) + } + } + }, + ExportNamedDeclaration: { + enter(path) { + const declaration = path.node.declaration + + // For "export function createResolvers() {}" + if ( + t.isFunctionDeclaration(declaration) && + t.isIdentifier(declaration.id, { name: `createResolvers` }) + ) { + insideCreateResolvers = true + } + + // For "export const createResolvers = function() {}" or "export const createResolvers = () => {}" + else if (t.isVariableDeclaration(declaration)) { + const declarators = declaration.declarations + for (const declarator of declarators) { + if ( + t.isIdentifier(declarator.id, { name: `createResolvers` }) && + (t.isFunctionExpression(declarator.init) || + t.isArrowFunctionExpression(declarator.init)) + ) { + insideCreateResolvers = true + } + } + } + }, + exit() { + insideCreateResolvers = false + }, + }, + TSInterfaceDeclaration(path, state) { + path.node.body.body.forEach(property => { + if ( + t.isTSPropertySignature(property) && + isContentTypeSelector(property.key.name) + ) { + property.key.name = updateContentfulSelector(property.key.name) + state.opts.hasChanged = true + } + }) + }, + TaggedTemplateExpression({ node }, state) { + if (node.tag.name !== `graphql`) { + return + } + const query = node.quasi?.quasis?.[0]?.value?.raw + if (query) { + const { ast: transformedGraphQLQuery, hasChanged } = + processGraphQLQuery(query, state) + + if (hasChanged) { + node.quasi.quasis[0].value.raw = graphql.print( + transformedGraphQLQuery + ) + state.opts.hasChanged = true + } + } + }, + CallExpression({ node }, state) { + if (node.callee.name !== `graphql`) { + return + } + const query = node.arguments?.[0].quasis?.[0]?.value?.raw + + if (query) { + const { ast: transformedGraphQLQuery, hasChanged } = + processGraphQLQuery(query, state) + + if (hasChanged) { + node.arguments[0].quasis[0].value.raw = graphql.print( + transformedGraphQLQuery + ) + state.opts.hasChanged = true + } + } + }, + }, + } +} + +// Locate a subfield within a selection set or fields +function locateSubfield(node, fieldName) { + const subFields = Array.isArray(node) + ? node + : node.selectionSet?.selections || node.value?.fields + if (!subFields) { + return null + } + return subFields.find(({ name }) => name?.value === fieldName) +} + +// Replace first old field occurence with new sys field +const injectSysField = (sysField, selections) => { + let sysInjected = false + + selections = selections + .map(field => { + const fieldName = field.name?.value || field.key?.name + if (fieldName === `sys`) { + const existingSysFields = ( + field.selectionSet?.selections || field.value.properties + ).map(subField => { + const kind = subField.kind || subField.type + // handle contentType rename + if ( + (kind === `ObjectProperty` + ? subField.key.name + : subField.name.value) === `contentType` + ) { + const subfields = + kind === `ObjectProperty` + ? subField.value.properties + : subField.selectionSet.selections + + subfields.map(contentTypeField => { + const fieldName = + kind === `ObjectProperty` + ? contentTypeField.key.name + : contentTypeField.name.value + + if (fieldName === `__typename`) { + if (kind === `ObjectProperty`) { + contentTypeField.key.name = `name` + } else { + contentTypeField.name.value = `name` + } + } + }) + } + return subField + }) + if (sysField?.type === `Property`) { + sysField.value.properties.push(...existingSysFields) + } else { + sysField.selectionSet.selections.push(...existingSysFields) + } + return null + } + return field + }) + .filter(Boolean) + + // Replace first old field occurence with new sys field + return selections + .map(field => { + const fieldName = field.name?.value || field.key?.name + if (SYS_FIELDS_TRANSFORMS.has(fieldName)) { + if (!sysInjected) { + // Inject for first occurence of a sys field + sysInjected = true + return sysField + } + // Remove all later fields + return null + } + // Keep non-sys fields as they are + return field + }) + .filter(Boolean) +} + +// Flatten the old deeply nested Contentful asset structure +const flattenAssetFields = node => { + const flatAssetFields = [] + + // Flatten asset file field + const fileField = locateSubfield(node, `file`) + + if (fileField) { + // Top level file fields + const urlField = locateSubfield(fileField, `url`) + if (urlField) { + flatAssetFields.push(urlField) + } + const fileNameField = locateSubfield(fileField, `fileName`) + if (fileNameField) { + flatAssetFields.push(fileNameField) + } + const contentTypeField = locateSubfield(fileField, `contentType`) + if (contentTypeField) { + flatAssetFields.push(contentTypeField) + } + + // details subfield with size and dimensions + const detailsField = locateSubfield(fileField, `details`) + if (detailsField) { + const sizeField = locateSubfield(detailsField, `size`) + if (sizeField) { + flatAssetFields.push(sizeField) + } + // width & height from image subfield + const imageField = locateSubfield(detailsField, `image`) + if (imageField) { + const widthField = locateSubfield(imageField, `width`) + if (widthField) { + flatAssetFields.push(widthField) + } + const heightField = locateSubfield(imageField, `height`) + if (heightField) { + flatAssetFields.push(heightField) + } + } + } + } + return flatAssetFields +} + +function createNewSysField(fields, fieldType = `Field`) { + const kind = fieldType === `Argument` ? `Argument` : `Field` + const subKindValue = fieldType === `Argument` ? `ObjectValue` : `SelectionSet` + const subKindIndex = fieldType === `Argument` ? `value` : `selectionSet` + const subKindIndex2 = fieldType === `Argument` ? `fields` : `selections` + + const contentfulSysFields = fields.filter(({ name }) => + SYS_FIELDS_TRANSFORMS.has(name?.value) + ) + + if (contentfulSysFields.length) { + const transformedSysFields = cloneDeep(contentfulSysFields).map(field => { + return { + ...field, + name: { + ...field.name, + value: SYS_FIELDS_TRANSFORMS.get(field.name.value), + }, + } + }) + + const sysField = { + kind: kind, + name: { + kind: `Name`, + value: `sys`, + }, + [subKindIndex]: { + kind: subKindValue, + [subKindIndex2]: transformedSysFields, + }, + } + return sysField + } + return null +} + +function processGraphQLQuery(query) { + try { + let hasChanged = false // this is sort of a hack, but print changes formatting and we only want to use it when we have to + const ast = graphql.parse(query) + + function processArguments(node) { + // flatten Contentful Asset filters + // Queries directly on allContentfulAssets + const flatAssetFields = flattenAssetFields(node) + if (flatAssetFields.length) { + node.value.fields = injectNewFields( + node.value.fields, + flatAssetFields, + `file` + ) + hasChanged = true + } + // Subfields that might be asset fields + node.value.fields.forEach((field, fieldIndex) => { + const flatAssetFields = flattenAssetFields(field) + if (flatAssetFields.length) { + node.value.fields[fieldIndex].value.fields = injectNewFields( + node.value.fields[fieldIndex].value.fields, + flatAssetFields, + `file` + ) + hasChanged = true + } + }) + + // Rename metadata -> contentfulMetadata + node.value.fields.forEach(field => { + if (field.name.value === `metadata`) { + field.name.value = `contentfulMetadata` + hasChanged = true + } + }) + + const sysField = createNewSysField(node.value.fields, `Argument`) + if (sysField) { + node.value.fields = injectSysField(sysField, node.value.fields) + hasChanged = true + } + } + + graphql.visit(ast, { + Argument(node) { + // Update filters and sort of collection endpoints + if ([`filter`, `sort`].includes(node.name.value)) { + if (node.value.kind === `ListValue`) { + node.value.values.forEach(node => processArguments({ value: node })) + return + } + processArguments(node) + } + }, + SelectionSet(node) { + // Rename content type node selectors + node.selections.forEach(field => { + if (isContentTypeSelector(field.name?.value)) { + field.name.value = updateContentfulSelector(field.name.value) + hasChanged = true + } + }) + }, + InlineFragment(node) { + if (isContentTypeSelector(node.typeCondition.name?.value)) { + node.typeCondition.name.value = updateContentfulSelector( + node.typeCondition.name.value + ) + hasChanged = true + + const sysField = createNewSysField(node.selectionSet.selections) + if (sysField) { + node.selectionSet.selections = injectSysField( + sysField, + node.selectionSet.selections + ) + hasChanged = true + } + } + }, + FragmentDefinition(node) { + if (isContentTypeSelector(node.typeCondition.name?.value)) { + node.typeCondition.name.value = updateContentfulSelector( + node.typeCondition.name.value + ) + hasChanged = true + + const sysField = createNewSysField(node.selectionSet.selections) + if (sysField) { + node.selectionSet.selections = injectSysField( + sysField, + node.selectionSet.selections + ) + hasChanged = true + } + } + }, + Field(node) { + // Flatten asset fields + if (node.name.value === `contentfulAsset`) { + const flatAssetFields = flattenAssetFields({ + value: { fields: node.arguments }, + }) + + node.arguments = injectNewFields( + node.arguments, + flatAssetFields, + `file` + ) + + hasChanged = true + } + + // Rename metadata -> contentfulMetadata + if (node.name.value === `metadata`) { + const tagsField = locateSubfield(node, `tags`) + if (tagsField) { + node.name.value = `contentfulMetadata` + hasChanged = true + } + } + + if (isContentTypeSelector(node.name.value)) { + // Move sys properties into new sys property + const nodesField = + node.name.value.indexOf(`all`) === 0 && + locateSubfield(node, `nodes`) + const rootNode = nodesField || node + + const sysField = createNewSysField(rootNode.selectionSet.selections) + if (sysField) { + rootNode.selectionSet.selections = injectSysField( + sysField, + rootNode.selectionSet.selections + ) + hasChanged = true + } + + const filterNode = + node.name.value.indexOf(`all`) === 0 && + locateSubfield(node.arguments, `filter`) + + const filterFields = filterNode?.value?.fields || node.arguments + + if (filterFields && filterFields.length) { + const sysField = createNewSysField(filterFields, `Argument`) + // Inject the new sys at the first occurence of any old sys field + if (sysField) { + if (node.name.value.indexOf(`all`) === 0) { + const filterFieldIndex = node.arguments.findIndex( + field => field.name?.value === `filter` + ) + node.arguments[filterFieldIndex].value.fields = injectSysField( + sysField, + node.arguments[filterFieldIndex].value.fields + ) + } else { + node.arguments = injectSysField(sysField, filterFields) + } + hasChanged = true + } + } + } + + // Flatten asset file field + const flatAssetFields = flattenAssetFields(node) + if (flatAssetFields.length) { + node.selectionSet.selections = injectNewFields( + node.selectionSet.selections, + flatAssetFields, + `file` + ) + hasChanged = true + } + }, + }) + return { ast, hasChanged } + } catch (err) { + throw new Error( + `GatsbySourceContentfulCodemod: GraphQL syntax error in query:\n\n${query}\n\nmessage:\n\n${err}` + ) + } +} + +function runParseSync(source, filePath) { + let ast + try { + ast = parseSync(source, { + plugins: [ + `@babel/plugin-syntax-jsx`, + `@babel/plugin-proposal-class-properties`, + ], + overrides: [ + { + test: [`**/*.ts`, `**/*.tsx`], + plugins: [[`@babel/plugin-syntax-typescript`, { isTSX: true }]], + }, + ], + filename: filePath, + parserOpts: { + tokens: true, // recast uses this + }, + }) + } catch (e) { + console.error(e) + } + if (!ast) { + console.log( + `The codemod was unable to parse ${filePath}. If you're running against the '/src' directory and your project has a custom babel config, try running from the root of the project so the codemod can pick it up.` + ) + } + return ast +} diff --git a/yarn.lock b/yarn.lock index 736204c98c8a1..48560fe05d3a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20334,7 +20334,7 @@ react-test-renderer@^16.14.0: react-is "^16.8.6" scheduler "^0.19.1" -react-typography@^0.16.23: +react-typography@^0.16.19, react-typography@^0.16.23: version "0.16.23" resolved "https://registry.yarnpkg.com/react-typography/-/react-typography-0.16.23.tgz#68c0af17ed876a30fcea256ef0dbbf0bc01b63b3" integrity sha512-nOl1/VDhqYSp5XktW/xmeLgE2S4L0LIWKAq2lNtc6Tq+3ZgvSX2Q0r0vlLKyOVq0hjsOYubE1TLoz/Wu/W8GLg== @@ -23771,11 +23771,29 @@ typescript@^5.0.4, typescript@^5.1.6: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== +typography-breakpoint-constants@^0.16.19: + version "0.16.19" + resolved "https://registry.yarnpkg.com/typography-breakpoint-constants/-/typography-breakpoint-constants-0.16.19.tgz#e0e89147749562cbb46ce76c47ff0f73372765e7" + integrity sha512-vXjfV9hwAXIOf5+U5GN137ahBkK+sj1TJu/5ksmP+8XB/D80lmGb/m0nKviWaQ3t7HLrK848VGrFS+6E2vcmVg== + typography-normalize@^0.16.19: version "0.16.19" resolved "https://registry.yarnpkg.com/typography-normalize/-/typography-normalize-0.16.19.tgz#58e0cf12466870c5b27006daa051fe7307780660" integrity sha512-vtnSv/uGBZVbd4e/ZhZB9HKBgKKlWQUXw74+ADIHHxzKp27CEf8PSR8TX1zF2qSyQ9/qMdqLwXYz8yRQFq9JLQ== +typography@^0.16.19: + version "0.16.24" + resolved "https://registry.yarnpkg.com/typography/-/typography-0.16.24.tgz#7bcbe3921367ca74168d0acce969749a0546fd4d" + integrity sha512-o5jNctzGoJm2XgdqivJdpkF6lQkcQo8v1biMGY+rLSpBHhpCKdQv5em9S3R6igApxVYtbhNBJbV95vK9oPwRKQ== + dependencies: + compass-vertical-rhythm "^1.4.5" + decamelize "^1.2.0" + gray-percentage "^2.0.0" + lodash "^4.13.1" + modularscale "^1.0.2" + object-assign "^4.1.0" + typography-normalize "^0.16.19" + typography@^0.16.21: version "0.16.21" resolved "https://registry.yarnpkg.com/typography/-/typography-0.16.21.tgz#2cd177f20c64d0b762389464688811f980ade682"