├── .babelrc
├── .github
└── FUNDING.yml
├── .gitignore
├── .prettierrc.json
├── README.md
├── package-lock.json
├── package.json
└── src
├── babel-loader.js
├── fragments.js
├── gatsby-node.js
├── gatsby-ssr.js
├── getRootQuery.js
├── graphql-nodes.js
├── index.js
├── pages.js
├── preview-template.js
├── preview.boilerplate.js
└── utils.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["babel-preset-gatsby-package"]
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: NathHorrigan
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /*.js
3 | .cache
4 | babel-plugin-remove-graphql-queries
5 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "tabWidth": 4,
4 | "singleQuote": true
5 | }
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # gatsby-source-wagtail
3 |
4 | > NOTE: This plugin requires that your Wagtail site use the [Wagtail-Grapple](https://github.com/torchbox/wagtail-grapple)
5 | library to build a compatible GraphQL endpoint. It does not work without a GraphQL endpoint.
6 |
7 | ## Features: 🚀
8 | * Stitches your Wagtail GraphQL endpoint into the internal Gatsby one.
9 | * Simple router that matches your Django models to Gatsby templates.
10 | * Redirect support, making your Wagtail redirects work with sites hosted on Netlify & S3.
11 | * Out-of-the-box Wagtail Preview with realtime update as you type in the admin.
12 | * Gatsby Image Support 🔥
13 | * Incremental builds using `GATSBY_EXPERIMENTAL_PAGE_BUILD_ON_DATA_CHANGES=true ` flag.
14 |
15 | ## How to use
16 |
17 | ### Installation
18 |
19 | `npm install gatsby-source-wagtail`
20 |
21 | ### Configuration
22 |
23 | Add the package to your `gatsby-config.js` with the url to your Wagtail GQL endpoint:
24 |
25 | ```js
26 | ...
27 | {
28 | resolve: "gatsby-source-wagtail",
29 | options: {
30 | url: "http://localhost:8000/graphql"
31 | },
32 | },
33 | ...
34 | ```
35 |
36 | #### Available Config Options
37 |
38 | | Option | Required | Description | Datatype | Default |
39 | |--------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|----------|
40 | | url | Y | The Wagtail GraphQL endpoint URL | string | null |
41 | | websocketUrl | N* | The GraphQL subscriptions endpoint URL. It can be inferred during development, but needs to be set in a production env | string | N/A |
42 | | headers | N | A JSON object of headers you want appended to all HTTP requests (Gatsby Build + Page Preview). | json | {} |
43 | | fieldName | N* | The field name you want your remote endpoint accessible under. If you have multiple connections then you will need to provide a value for at least one of them. | string | wagtail |
44 | | typeName | N* | The remote schema's internal type name. When you have multiple connections, you will need to provide a value (just copy fieldName). | string | wagtail |
45 | | isDefault | N* | A settings that tells the plugin which Wagtail GraphQL endpoint is the primary/default one. Used for preview and page generation. If you have multiple connections, you must choose which one you will generate pages from. Multiple site page generation is planned for future development. | string | true |
46 |
47 |
48 | ### Page Router
49 | This source plugin provides a simple router that maps a Django model to a specific Gatsby template. Pass a JSON map to the function in your `gatsby-node.js`.
50 | The router also adds Wagtail Preview to your Gatsby site automagically! Now point your backend to the Gatsby site and everything will work: [How to link Wagtail & Gatsby](LINK TO BACKEND DOCS).
51 |
52 | To map a Django model with the `home.BlogPage` ContentType to a template located at `./src/templates/blog.js`
53 |
54 | ```js
55 | const { createWagtailPages } = require("gatsby-source-wagtail/pages.js")
56 |
57 | exports.createPages = ({ graphql, actions }) => {
58 | return createWagtailPages({
59 | "home.BlogPage": "templates/blog.js",
60 | }, graphql, actions, [])
61 | }
62 | ```
63 |
64 | The example template:
65 |
66 |
67 | ```jsx
68 | ...
69 |
70 | export default ({ data }) => {
71 | const { page } = data.wagtail
72 |
73 | return (
74 |
75 |
{ page.title }
76 |
77 | )
78 | }
79 |
80 | export const query = graphql`
81 | query($slug: String) {
82 | wagtail {
83 | page(slug: $slug) {
84 | ...on BlogPage {
85 | title
86 | }
87 | }
88 | }
89 | }
90 | `
91 | ```
92 |
93 | Some page specific information is passed to page through the Gatsby context prop. The following variables are passed, thus are available in templates:
94 |
95 | * $id: Int
96 | * $slug: String
97 | * $url: String
98 | * $contentType: String
99 |
100 | ### Redirects
101 | The plugin queries your Wagtail endpoint for any defined redirects and pass them to the Gatsby `createRedirect` function.
102 |
103 | ### Image Fragments
104 | You can take advantage of the [Gatsby Image](https://www.gatsbyjs.org/packages/gatsby-image/) processing abilites by allowing Gatsby to download your images and progressively enhance them on the page.
105 |
106 | ```jsx
107 | import React from "react"
108 | import { graphql } from "gatsby"
109 | import Img from "gatsby-image"
110 |
111 | export default function BlogTemplate({ data }) {
112 | const page = data.wagtail.page
113 | return (
114 |
115 | page?.title
116 |
117 |
118 | )
119 | }
120 |
121 | export const query = graphql`
122 | query BlogIndexQuery($slug: String) {
123 | wagtail {
124 | page(slug: $slug) {
125 | ...on BlogPage {
126 | title
127 | cover {
128 | imageFile {
129 | childImageSharp {
130 | square: fixed(width: 300, height: 300) {
131 | ...GatsbyImageSharpFixed
132 | }
133 | }
134 | }
135 | }
136 | }
137 | }
138 | }
139 | }
140 | `
141 | ```
142 |
143 | `gatsby-transformer-sharp` and `gatsby-plugin-sharp` are required for local image processing.
144 |
145 | The following fragments work with `gatsby-source-wagtail`:
146 | * GatsbyImageSharpFixed
147 | * GatsbyImageSharpFixed_noBase64
148 | * GatsbyImageSharpFixed_tracedSVG
149 | * GatsbyImageSharpFixed_withWebp
150 | * GatsbyImageSharpFixed_withWebp_noBase64
151 | * GatsbyImageSharpFixed_withWebp_tracedSVG
152 | * GatsbyImageSharpFluid
153 | * GatsbyImageSharpFluid_noBase64
154 | * GatsbyImageSharpFluid_tracedSVG
155 | * GatsbyImageSharpFluid_withWebp
156 | * GatsbyImageSharpFluid_withWebp_noBase64
157 | * GatsbyImageSharpFluid_withWebp_tracedSVG
158 | * GatsbyImageSharpFluidLimitPresentationSize
159 |
160 | When previewing the page using Wagtail Preview, the image processing is mocked and the plugin will use the raw source files from your Wagtail's media host. It should, however, respect the image dimension constraints.
161 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby-source-wagtail",
3 | "description": "Gatsby plugin which adds Wagtail Grapple support to Gatsby",
4 | "version": "0.5.1",
5 | "author": "NathHorrigan ",
6 | "bugs": {
7 | "url": "https://github.com/NathHorrigan/gatsby-source-wagtail/issues"
8 | },
9 | "main": "index.js",
10 | "files": [
11 | "*.js",
12 | "babel-plugin-remove-graphql-queries/*.js",
13 | "index.d.ts"
14 | ],
15 | "dependencies": {
16 | "@babel/runtime": "^7.12.1",
17 | "apollo-boost": "^0.4.9",
18 | "apollo-cache-inmemory": "^1.6.6",
19 | "apollo-client": "^2.6.10",
20 | "apollo-link-http": "^1.5.17",
21 | "apollo-link-ws": "^1.0.20",
22 | "apollo-utilities": "^1.3.4",
23 | "babel-plugin-remove-graphql-queries": "^2.9.20",
24 | "fs-extra": "^8.1.0",
25 | "gatsby": "^2.25.0",
26 | "gatsby-source-filesystem": "^2.4.0",
27 | "gatsby-source-graphql": "^2.7.6",
28 | "graphql": "^14.7.0",
29 | "graphql-2-json-schema": "^0.2.0",
30 | "graphql-request": "^3.3.0",
31 | "graphql-tag": "^2.11.0",
32 | "graphql-tools": "^3.1.1",
33 | "lodash": "^4.17.20",
34 | "lodash.clonedeep": "4.5.0",
35 | "lodash.get": "^4.4.2",
36 | "lodash.merge": "^4.6.2",
37 | "query-string": "^6.13.6",
38 | "react": "^16.14.0",
39 | "subscriptions-transport-ws": "^0.9.18",
40 | "traverse": "^0.6.6"
41 | },
42 | "devDependencies": {
43 | "@babel/cli": "^7.12.1",
44 | "@babel/core": "^7.12.3",
45 | "@babel/plugin-syntax-dynamic-import": "^7.8.3",
46 | "babel-preset-gatsby-package": "^0.1.4",
47 | "cross-env": "^5.2.1",
48 | "prettier": "2.0.5"
49 | },
50 | "homepage": "https://github.com/NathHorrigan/gatsby-source-wagtail//#readme",
51 | "keywords": [
52 | "gatsby",
53 | "gatsby-plugin",
54 | "graphql",
55 | "universal"
56 | ],
57 | "peerDependencies": {
58 | "gatsby": "^2.13.77"
59 | },
60 | "resolutions": {
61 | "graphql": "14.5.0"
62 | },
63 | "license": "MIT",
64 | "repository": "https://github.com/NathHorrigan/gatsby-source-wagtail/",
65 | "scripts": {
66 | "build": "babel src/ --out-dir . --ignore **/__tests__",
67 | "prepare": "cross-env NODE_ENV=production npm run build",
68 | "watch": "babel -w src --out-dir . --ignore **/__tests__",
69 | "format": "prettier --write src/**/*.js",
70 | "clean": "rm -rf ./*.js"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/babel-loader.js:
--------------------------------------------------------------------------------
1 | const babelLoader = require(`babel-loader`)
2 |
3 | const {
4 | getCustomOptions,
5 | mergeConfigItemOptions,
6 | } = require(`gatsby/dist/utils/babel-loader-helpers`)
7 | const { prepareOptions } = require(`./utils`);
8 |
9 | /**
10 | * Gatsby's custom loader for webpack & babel
11 | *
12 | * Gatsby allows sites to either use our Babel setup (the default)
13 | * or to add a .babelrc to take control.
14 | *
15 | * Our default setup is defined in the fallbackPlugins/fallbackPresets arrays
16 | * below.
17 | *
18 | * After using either the fallback or user supplied setup, we add on a handful
19 | * of required plugins and finally merge in any presets/plugins supplied
20 | * by Gatsby plugins.
21 | *
22 | * You can find documentation for the custom loader here: https://babeljs.io/docs/en/next/babel-core.html#loadpartialconfig
23 | */
24 | module.exports = babelLoader.custom((babel) => {
25 | const toReturn = {
26 | // Passed the loader options.
27 | customOptions({ stage = `test`, reactRuntime = `classic`, ...options }) {
28 | return {
29 | custom: {
30 | stage,
31 | reactRuntime
32 | },
33 | loader: {
34 | cacheDirectory: true,
35 | sourceType: `unambiguous`,
36 | ...getCustomOptions(stage),
37 | },
38 | ...options
39 | }
40 | },
41 |
42 | // Passed Babel's 'PartialConfig' object.
43 | config(partialConfig, { customOptions }) {
44 | let { options } = partialConfig
45 | const [
46 | reduxPresets,
47 | reduxPlugins,
48 | requiredPresets,
49 | requiredPlugins,
50 | fallbackPresets,
51 | ] = prepareOptions(babel, customOptions)
52 |
53 | // If there is no filesystem babel config present, add our fallback
54 | // presets/plugins.
55 | if (!partialConfig.hasFilesystemConfig()) {
56 | options = {
57 | ...options,
58 | plugins: requiredPlugins,
59 | presets: [...fallbackPresets, ...requiredPresets],
60 | }
61 | } else {
62 | // With a babelrc present, only add our required plugins/presets
63 | options = {
64 | ...options,
65 | plugins: [...options.plugins, ...requiredPlugins],
66 | presets: [...options.presets, ...requiredPresets],
67 | }
68 | }
69 |
70 | // Merge in presets/plugins added from gatsby plugins.
71 | reduxPresets.forEach(preset => {
72 | options.presets = mergeConfigItemOptions({
73 | items: options.presets,
74 | itemToMerge: preset,
75 | type: `preset`,
76 | babel,
77 | })
78 | })
79 |
80 | reduxPlugins.forEach(plugin => {
81 | options.plugins = mergeConfigItemOptions({
82 | items: options.plugins,
83 | itemToMerge: plugin,
84 | type: `plugin`,
85 | babel,
86 | })
87 | })
88 |
89 | return options
90 | },
91 | }
92 |
93 | return toReturn
94 | })
95 |
--------------------------------------------------------------------------------
/src/fragments.js:
--------------------------------------------------------------------------------
1 | exports.generateImageFragments = type => `
2 | import { graphql } from 'gatsby'
3 |
4 | export const query = graphql\`
5 |
6 | fragment WagtailImageFixed on ${type} {
7 | base64
8 | width
9 | height
10 | src
11 | srcSet(sizes: [300, 400, 800, 1400])
12 | }
13 |
14 | fragment WagtailImageFixed_tracedSVG on ${type} {
15 | width
16 | height
17 | src
18 | srcSet(sizes: [300, 400, 800, 1400])
19 | tracedSVG
20 | }
21 |
22 | fragment WagtailImageFixed_noBase64 on ${type} {
23 | width
24 | height
25 | src
26 | srcSet(sizes: [300, 400, 800, 1400])
27 | }
28 |
29 | fragment WagtailImageFluid on ${type} {
30 | base64
31 | aspectRatio
32 | src
33 | srcSet(sizes: [300, 400, 800, 1400])
34 | sizes
35 | }
36 |
37 | fragment WagtailImageFluid_tracedSVG on ${type} {
38 | tracedSVG
39 | aspectRatio
40 | src
41 | srcSet(sizes: [300, 400, 800, 1400])
42 | sizes
43 | }
44 |
45 | fragment WagtailImageFluid_noBase64 on ${type} {
46 | aspectRatio
47 | src
48 | srcSet(sizes: [300, 400, 800, 1400])
49 | sizes
50 | }
51 | \`
52 | `
53 |
--------------------------------------------------------------------------------
/src/gatsby-node.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra')
2 | const request = require('graphql-request')
3 | const { createRemoteFileNode } = require('gatsby-source-filesystem')
4 | const { sourceNodes } = require('./graphql-nodes')
5 | const { getRootQuery } = require('./getRootQuery')
6 | const { generateImageFragments } = require('./fragments')
7 | const fetch = require(`node-fetch`)
8 |
9 | const queryBackend = (query, url, headers) =>
10 | fetch(url, {
11 | method: 'POST',
12 | headers: { 'Content-Type': 'application/json', ...headers },
13 | body: JSON.stringify({
14 | variables: {},
15 | query
16 | })
17 | }).then(result => result.json())
18 |
19 | // Monkeypatch options to allow default fieldName and typeName
20 | exports.onPreInit = ({}, options) => {
21 | options.fieldName = options.fieldName || 'wagtail'
22 | options.typeName = options.typeName || 'wagtail'
23 | }
24 |
25 | exports.sourceNodes = sourceNodes
26 |
27 | exports.onCreateWebpackConfig = ({ stage, actions, getConfig }) => {
28 | const config = getConfig()
29 | if (stage.indexOf('html') >= 0) {
30 | return
31 | }
32 |
33 | const replaceRule = ruleUse => {
34 | if (
35 | ruleUse.loader &&
36 | ruleUse.loader.indexOf(`gatsby/dist/utils/babel-loader.js`) >= 0
37 | ) {
38 | ruleUse.loader = require.resolve(
39 | `gatsby-source-wagtail/babel-loader.js`
40 | )
41 | }
42 | }
43 |
44 | const traverseRule = rule => {
45 | if (rule.oneOf && Array.isArray(rule.oneOf)) {
46 | rule.oneOf.forEach(traverseRule)
47 | }
48 |
49 | if (rule.use) {
50 | if (Array.isArray(rule.use)) {
51 | rule.use.forEach(replaceRule)
52 | } else {
53 | replaceRule(rule.use)
54 | }
55 | }
56 | }
57 | config.module.rules.forEach(traverseRule)
58 | actions.replaceWebpackConfig(config)
59 | }
60 |
61 | exports.onPreExtractQueries = async ({ store, actions }, options) => {
62 | const { createRedirect } = actions
63 | return queryBackend(
64 | `{
65 | imageType
66 | redirects {
67 | oldPath
68 | newUrl
69 | isPermanent
70 | }
71 | __schema {
72 | types {
73 | kind
74 | name
75 | fields {
76 | name
77 | }
78 | possibleTypes {
79 | name
80 | }
81 | }
82 | }
83 | }`,
84 | options.url,
85 | options.headers
86 | ).then(({ data }) => {
87 | // Check if fields added by wagtail-gatsby are visible
88 | const wagtailGatsbyInstalled = !!data.__schema.types
89 | .find(objectType => objectType.name == data.imageType)
90 | .fields.find(field => field.name == 'tracedSVG')
91 |
92 | // Build schema file for Apollo, here we're filtering out any type information unrelated to unions or interfaces
93 | const filteredData = data.__schema.types.filter(
94 | type => type.possibleTypes !== null
95 | )
96 | data.__schema.types = filteredData
97 | fs.writeFile(
98 | './node_modules/gatsby-source-wagtail/fragmentTypes.json',
99 | JSON.stringify(data),
100 | err => {
101 | if (err) {
102 | console.error(
103 | 'Gatsby-source-wagtail: Error writing fragmentTypes file',
104 | err
105 | )
106 | }
107 | }
108 | )
109 |
110 | // Generate Image Fragments for the servers respective image model.
111 | const program = store.getState().program
112 | const fragments = wagtailGatsbyInstalled
113 | ? generateImageFragments(data.imageType)
114 | : ''
115 | fs.writeFile(
116 | `${program.directory}/.cache/fragments/gatsby-source-wagtail-fragments.js`,
117 | fragments,
118 | err => {
119 | if (err) console.error(err)
120 | }
121 | )
122 |
123 | // Copy the boilerplate file and replace the placeholder with actual modal name
124 | fs.readFile(
125 | './node_modules/gatsby-source-wagtail/preview.boilerplate.js',
126 | (err, fileData) => {
127 | if (err)
128 | return console.error(
129 | 'Could not read preview boilerplate file',
130 | err
131 | )
132 | // Replace placeholder
133 | let jsFile = fileData
134 | .toString()
135 | .replace('CustomImage', data.imageType)
136 | // Rewrite file so it's accessible
137 | fs.writeFile(
138 | `./node_modules/gatsby-source-wagtail/preview.js`,
139 | jsFile,
140 | err => {
141 | if (err)
142 | console.error('Could not write preview file', err)
143 | }
144 | )
145 | }
146 | )
147 |
148 | // Generate redirects for Netlify, controlled by Wagtail Admin.
149 | data.redirects.map(redirect =>
150 | createRedirect({
151 | fromPath: redirect.oldPath,
152 | toPath: redirect.newUrl,
153 | isPermanent: redirect.isPermanent,
154 | force: true
155 | })
156 | )
157 | })
158 | }
159 |
160 | exports.createResolvers = (
161 | { actions, getCache, createNodeId, createResolvers, store, reporter },
162 | options
163 | ) => {
164 | const { createNode } = actions
165 | return queryBackend(
166 | `{
167 | imageType
168 | }`,
169 | options.url,
170 | options.headers
171 | ).then(({ data }) => {
172 | createResolvers({
173 | [data.imageType]: {
174 | imageFile: {
175 | type: `File`,
176 | resolve(source, args, context, info) {
177 | return createRemoteFileNode({
178 | url: source.src,
179 | store,
180 | getCache,
181 | createNode,
182 | createNodeId
183 | }).catch(err => console.error(err))
184 | }
185 | }
186 | }
187 | })
188 | })
189 | }
190 |
--------------------------------------------------------------------------------
/src/gatsby-ssr.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | exports.onRenderBody = ({ setHeadComponents }, pluginOptions) => {
4 | const {
5 | typeName,
6 | fieldName,
7 | isDefault = true,
8 | url,
9 | websocketUrl = null,
10 | headers
11 | } = pluginOptions
12 |
13 | const connectionName = isDefault ? 'default' : fieldName
14 |
15 | setHeadComponents([
16 |
24 | ])
25 | }
26 |
--------------------------------------------------------------------------------
/src/getRootQuery.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const get = require('lodash.get')
3 | const { babelParseToAst } = require('gatsby/dist/utils/babel-parse-to-ast')
4 |
5 | const getRootQuery = componentPath => {
6 | try {
7 | const content = fs.readFileSync(componentPath, 'utf-8')
8 | const ast = babelParseToAst(content, componentPath)
9 | const exported = get(ast, 'program.body', []).filter(
10 | n => n.type === 'ExportNamedDeclaration'
11 | )
12 | const exportedQuery = exported.find(
13 | exp => get(exp, 'declaration.declarations.0.id.name') === 'query'
14 | )
15 |
16 | if (exportedQuery) {
17 | const query = get(
18 | exportedQuery,
19 | 'declaration.declarations.0.init.quasi.quasis.0.value.raw'
20 | )
21 |
22 | if (query) {
23 | return query
24 | }
25 | }
26 | } catch (err) {
27 | if (err) console.error(err)
28 | }
29 | return null
30 | }
31 |
32 | exports.getRootQuery = getRootQuery
33 |
--------------------------------------------------------------------------------
/src/graphql-nodes.js:
--------------------------------------------------------------------------------
1 | const uuidv4 = require(`uuid/v4`)
2 | const {
3 | buildSchema,
4 | printSchema,
5 | graphqlSync,
6 | introspectionQuery,
7 | IntrospectionQuery
8 | } = require(`graphql`)
9 | const { fromIntrospectionQuery } = require('graphql-2-json-schema')
10 |
11 | const {
12 | makeRemoteExecutableSchema,
13 | delegateToSchema,
14 | transformSchema,
15 | introspectSchema,
16 | RenameTypes,
17 | mergeSchemas
18 | } = require(`graphql-tools`)
19 | const { createHttpLink } = require(`apollo-link-http`)
20 | const fetch = require(`node-fetch`)
21 | const invariant = require(`invariant`)
22 | const traverse = require(`traverse`)
23 |
24 | const {
25 | NamespaceUnderFieldTransform,
26 | StripNonQueryTransform
27 | } = require(`gatsby-source-graphql/transforms`)
28 |
29 | const { createSelection } = require(`./utils`)
30 |
31 | exports.sourceNodes = async (
32 | { actions, createNodeId, cache, createContentDigest },
33 | options
34 | ) => {
35 | const { addThirdPartySchema, createNode } = actions
36 | const {
37 | url,
38 | typeName,
39 | fieldName,
40 | headers = {},
41 | fetchOptions = {},
42 | createLink,
43 | createSchema,
44 | refetchInterval
45 | } = options
46 |
47 | invariant(
48 | typeName && typeName.length > 0,
49 | `gatsby-source-wagtail requires option \`typeName\` to be specified`
50 | )
51 | invariant(
52 | fieldName && fieldName.length > 0,
53 | `gatsby-source-wagtail requires option \`fieldName\` to be specified`
54 | )
55 | invariant(
56 | (url && url.length > 0) || createLink,
57 | `gatsby-source-wagtail requires either option \`url\` or \`createLink\` callback`
58 | )
59 |
60 | let link
61 | if (createLink) {
62 | link = await createLink(options)
63 | } else {
64 | link = createHttpLink({
65 | uri: url,
66 | fetch,
67 | headers,
68 | fetchOptions
69 | })
70 | }
71 |
72 | let introspectionSchema
73 |
74 | if (createSchema) {
75 | introspectionSchema = await createSchema(options)
76 | } else {
77 | const cacheKey = `gatsby-source-wagtail-${typeName}-${fieldName}`
78 | let sdl = await cache.get(cacheKey)
79 |
80 | // Cache the remote schema for performance benefit
81 | if (!sdl) {
82 | introspectionSchema = await introspectSchema(link)
83 | sdl = printSchema(introspectionSchema)
84 | } else {
85 | introspectionSchema = buildSchema(sdl)
86 | }
87 |
88 | await cache.set(cacheKey, sdl)
89 | }
90 |
91 | // Create a remote link to the Wagtail GraphQL schema
92 | const remoteSchema = makeRemoteExecutableSchema({
93 | schema: introspectionSchema,
94 | link
95 | })
96 |
97 | // Create a point in the schema that can be used to access Wagtail
98 | const nodeId = createNodeId(`gatsby-source-wagtail-${typeName}`)
99 | const node = createSchemaNode({
100 | id: nodeId,
101 | typeName,
102 | fieldName,
103 | createContentDigest
104 | })
105 | createNode(node)
106 |
107 | const resolver = (parent, args, context) => {
108 | context.nodeModel.createPageDependency({
109 | path: context.path,
110 | nodeId: nodeId
111 | })
112 | return {}
113 | }
114 |
115 | // Add some customization of the remote schema
116 | let transforms = []
117 | if (options.prefixTypename) {
118 | transforms = [
119 | new StripNonQueryTransform(),
120 | new RenameTypes(name => `${typeName}_${name}`),
121 | new NamespaceUnderFieldTransform({
122 | typeName,
123 | fieldName,
124 | resolver
125 | }),
126 | new WagtailRequestTransformer()
127 | ]
128 | } else {
129 | transforms = [
130 | new StripNonQueryTransform(),
131 | new NamespaceUnderFieldTransform({
132 | typeName,
133 | fieldName,
134 | resolver
135 | }),
136 | new WagtailRequestTransformer()
137 | ]
138 | }
139 |
140 | const mergeLocalAndRemoteSchema = async () => {
141 | // merge the schema along with custom resolvers
142 | const schema = mergeSchemas({
143 | schemas: [remoteSchema]
144 | })
145 |
146 | // Apply any transforms
147 | return transformSchema(schema, transforms)
148 | }
149 |
150 | // Add new merged schema to Gatsby
151 | addThirdPartySchema({
152 | schema: await mergeLocalAndRemoteSchema()
153 | })
154 |
155 | // Allow refreshing of the remote data in DEV mode only
156 | if (process.env.NODE_ENV !== `production`) {
157 | if (refetchInterval) {
158 | const msRefetchInterval = refetchInterval * 1000
159 | const refetcher = () => {
160 | createNode(
161 | createSchemaNode({
162 | id: nodeId,
163 | typeName,
164 | fieldName,
165 | createContentDigest
166 | })
167 | )
168 | setTimeout(refetcher, msRefetchInterval)
169 | }
170 | setTimeout(refetcher, msRefetchInterval)
171 | }
172 | }
173 | }
174 |
175 | function createSchemaNode({ id, typeName, fieldName, createContentDigest }) {
176 | const nodeContent = uuidv4()
177 | const nodeContentDigest = createContentDigest(nodeContent)
178 | return {
179 | id,
180 | typeName: typeName,
181 | fieldName: fieldName,
182 | parent: null,
183 | children: [],
184 | internal: {
185 | type: `GraphQLSource`,
186 | contentDigest: nodeContentDigest,
187 | ignoreType: true
188 | }
189 | }
190 | }
191 |
192 | class WagtailRequestTransformer {
193 | transformSchema = schema => schema
194 | transformRequest = request => {
195 | for (let node of traverse(request.document.definitions).nodes()) {
196 | if (
197 | node?.kind == 'Field' &&
198 | node?.selectionSet?.selections?.find(
199 | selection => selection?.name?.value == 'imageFile'
200 | )
201 | ) {
202 | // Add field to AST
203 | const createSelection = name => ({
204 | kind: 'Field',
205 | name: {
206 | kind: 'Name',
207 | value: name
208 | },
209 | arguments: [],
210 | directives: []
211 | })
212 | // Make sure we have src, height & width details
213 | node.selectionSet.selections.push(createSelection('id'))
214 | node.selectionSet.selections.push(createSelection('src'))
215 | // Break as we don't need to visit any other nodes
216 | break
217 | }
218 | }
219 |
220 | return request
221 | }
222 | transformResult = result => result
223 | }
224 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import gql from 'graphql-tag'
3 | import traverse from 'traverse'
4 | import cloneDeep from 'lodash.clonedeep'
5 | import PropTypes from 'prop-types'
6 | export { withPreview, decodePreviewUrl } from './preview'
7 |
8 | const options = new Map()
9 |
10 | export const getOptions = name => {
11 | if (!options.has(name)) {
12 | if (typeof window !== 'undefined') {
13 | setOptions(name, window.___wagtail[name])
14 | }
15 | }
16 |
17 | return options.get(name)
18 | }
19 |
20 | export const setOptions = (name, opts) => {
21 | if (!opts) {
22 | throw new Error('Wagtail: No options "' + name + '".')
23 | }
24 | if (!opts.client && !opts.url) {
25 | throw new Error('Wagtail: Could not get "url" for "' + name + '".')
26 | }
27 | if (!opts.typeName) {
28 | throw new Error('Wagtail: Could not get "typeName" for "' + name + '".')
29 | }
30 |
31 | options.set(name, opts)
32 | }
33 |
34 | export const getQuery = query => {
35 | if (typeof query === 'object' && query.definitions) {
36 | return query
37 | } else if (typeof query === 'string') {
38 | return gql(query)
39 | } else if (typeof query === 'object' && query.source) {
40 | return gql(query.source)
41 | } else {
42 | throw new Error('Could not parse query: ' + query)
43 | }
44 | }
45 |
46 | export const getIsolatedQuery = (querySource, fieldName, typeName) => {
47 | const query = getQuery(querySource)
48 | const updatedQuery = cloneDeep(query)
49 |
50 | const updatedRoot = updatedQuery.definitions[0].selectionSet.selections.find(
51 | selection =>
52 | selection.name &&
53 | selection.name.kind === 'Name' &&
54 | selection.name.value === fieldName
55 | )
56 |
57 | if (updatedRoot) {
58 | updatedQuery.definitions[0].selectionSet.selections =
59 | updatedRoot.selectionSet.selections
60 | } else if (fieldName) {
61 | console.warn('Failed to update query root')
62 | return
63 | }
64 |
65 | traverse(updatedQuery).forEach(function(x) {
66 | if (this.isLeaf && this.parent && this.parent.key === 'name') {
67 | if (
68 | this.parent.parent &&
69 | this.parent.parent.node.kind === 'NamedType'
70 | ) {
71 | if (typeof x === 'string' && x.indexOf(`${typeName}_`) === 0) {
72 | this.update(x.substr(typeName.length + 1))
73 | }
74 | }
75 | }
76 | })
77 |
78 | return updatedQuery
79 | }
80 |
--------------------------------------------------------------------------------
/src/pages.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | export const createWagtailPages = (
4 | pageMap,
5 | graphql,
6 | actions,
7 | fragmentFiles
8 | ) => {
9 | return graphql(`
10 | {
11 | wagtail {
12 | pages {
13 | lastPublishedAt
14 | contentType
15 | url
16 | slug
17 | id
18 | }
19 | }
20 | }
21 | `).then(res => {
22 | const { createPage } = actions
23 |
24 | if (res.data.wagtail.pages) {
25 | const pages = res.data.wagtail.pages
26 |
27 | // Create pages for any page objects that match the page-map.
28 | pages.map(page => {
29 | const matchingKey = Object.keys(pageMap).find(
30 | key => key.toLowerCase() == page.contentType.toLowerCase()
31 | )
32 |
33 | if (matchingKey) {
34 | const template = pageMap[matchingKey]
35 | createPage({
36 | path: page.url || '/',
37 | matchPath: page.url || '/',
38 | component: path.resolve('./src/' + template),
39 | lastPublishedAt: page.lastPublishedAt,
40 | context: page
41 | })
42 | }
43 | })
44 |
45 | // Create preview page and pass page-map.
46 | createPage({
47 | path: '/preview',
48 | component: path.resolve(
49 | './node_modules/gatsby-source-wagtail/preview-template.js'
50 | ),
51 | context: { pageMap, fragmentFiles }
52 | })
53 | } else {
54 | console.log('Could not read any Wagtail Pages from query!')
55 | }
56 | })
57 | }
58 |
--------------------------------------------------------------------------------
/src/preview-template.js:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react'
2 | import { decodePreviewUrl, withPreview } from './preview'
3 | import { query as wagtailBaseFragments } from '../../.cache/fragments/gatsby-source-wagtail-fragments.js'
4 |
5 | class PreviewPage extends React.Component {
6 | state = {
7 | Component: () => null,
8 | fragments: wagtailBaseFragments?.source || ''
9 | }
10 |
11 | componentDidMount() {
12 | if (typeof window != `undefined`) {
13 | // Fetch the fragment files specified by the user
14 | const { fragmentFiles } = this.props.pageContext
15 | const fragmentModules = fragmentFiles.map(file =>
16 | require('../../src/' + file)
17 | )
18 | this.fetchFragments(fragmentModules)
19 | // Get the correct template component
20 | this.fetchComponent()
21 | }
22 | }
23 |
24 | fetchFragments = fragmentModules => {
25 | fragmentModules.map(mod => {
26 | Object.keys(mod).map(exportKey => {
27 | const exportObj = mod[exportKey]
28 | if (typeof exportObj.source == 'string') {
29 | this.setState({
30 | fragments: (this.state.fragments +=
31 | exportObj?.source || '')
32 | })
33 | }
34 | })
35 | })
36 | }
37 |
38 | fetchComponent = () => {
39 | const { pageMap } = this.props.pageContext
40 | const { content_type } = decodePreviewUrl()
41 | const pageMapKey = Object.keys(pageMap).find(
42 | key => key.toLowerCase() == content_type.toLowerCase()
43 | )
44 |
45 | const componentFile = require('../../src/' + pageMap[pageMapKey])
46 | this.setState({
47 | Component: withPreview(
48 | componentFile.default,
49 | componentFile.query,
50 | this.state.fragments
51 | )
52 | })
53 | }
54 |
55 | render() {
56 | const { Component } = this.state
57 | return
58 | }
59 | }
60 |
61 | export default PreviewPage
62 |
--------------------------------------------------------------------------------
/src/preview.boilerplate.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import qs from 'querystring'
3 | import cloneDeep from 'lodash.clonedeep'
4 | import merge from 'lodash.merge'
5 | import traverse from 'traverse'
6 |
7 | import { ApolloClient } from 'apollo-client'
8 | import { gql } from 'apollo-boost'
9 | import { split } from 'apollo-link'
10 | import {
11 | InMemoryCache,
12 | IntrospectionFragmentMatcher,
13 | } from 'apollo-cache-inmemory'
14 | import { WebSocketLink } from 'apollo-link-ws'
15 | import { getMainDefinition } from 'apollo-utilities'
16 | import { createHttpLink } from 'apollo-link-http'
17 | import {
18 | introspectSchema,
19 | makeRemoteExecutableSchema,
20 | mergeSchemas,
21 | } from 'graphql-tools'
22 |
23 | import { print } from 'graphql/language/printer'
24 | import { getQuery, getIsolatedQuery } from './index'
25 | import introspectionQueryResultData from './fragmentTypes.json'
26 |
27 | const PreviewProvider = async (query, fragments = '', onNext) => {
28 | // Extract query from wagtail schema
29 | const {
30 | typeName,
31 | fieldName,
32 | url,
33 | websocketUrl,
34 | headers,
35 | } = window.___wagtail.default
36 | const isolatedQuery = getIsolatedQuery(query, fieldName, typeName)
37 | const { content_type, token } = decodePreviewUrl()
38 |
39 | // Generate auth token for basic auth.
40 | const getToken = () => {
41 | const username = process.env.GATSBY_AUTH_USER
42 | const password = process.env.GATSBY_AUTH_PASS
43 | return btoa(username + ':' + password)
44 | }
45 |
46 | // Normal query link
47 | let link = createHttpLink({
48 | uri: url,
49 | fetchOptions: {
50 | headers: { Authorization: token ? `Basic ${getToken()}` : '' },
51 | },
52 | })
53 |
54 | // If provided create a subscription endpoint
55 | if (websocketUrl) {
56 | // Link used for subscriptions
57 | const wsLink = new WebSocketLink({
58 | uri: websocketUrl,
59 | options: {
60 | reconnect: true,
61 | connectionParams: {
62 | authToken: getToken(),
63 | },
64 | },
65 | })
66 |
67 | // Alias original link and create one that merges the two
68 | const httpLink = link
69 | link = split(
70 | // split based on operation type
71 | ({ query }) => {
72 | const definition = getMainDefinition(query)
73 | return (
74 | definition.kind === 'OperationDefinition' &&
75 | definition.operation === 'subscription'
76 | )
77 | },
78 | wsLink,
79 | httpLink
80 | )
81 | }
82 |
83 | // Mock the fieldName for accessing local image files
84 | const typeDefs = gql`
85 | type File {
86 | sourceInstanceName: String!
87 | absolutePath: String!
88 | relativePath: String!
89 | size: Int!
90 | prettySize: String!
91 | modifiedTime: Date!
92 | accessTime: Date!
93 | changeTime: Date!
94 | root: String!
95 | dir: String!
96 | base: String!
97 | ext: String!
98 | name: String!
99 | extension: String!
100 | relativeDirectory: String!
101 | url: String
102 | publicUrl: String
103 | childImageSharp: ImageSharp
104 | id: ID!
105 | parent: Node
106 | children: [Node!]!
107 | }
108 |
109 | type Node {
110 | id: ID!
111 | }
112 |
113 | type ImageSharp {
114 | fluid: ImageSharpFluid!
115 | fixed: ImageSharpFixed!
116 | }
117 |
118 | type ImageSharpFluid {
119 | base64: String
120 | tracedSVG: String
121 | aspectRatio: Float!
122 | src: String!
123 | srcSet: String!
124 | srcWebp: String
125 | srcSetWebp: String
126 | sizes: String!
127 | originalImg: String
128 | originalName: String
129 | presentationWidth: Int!
130 | presentationHeight: Int!
131 | }
132 |
133 | type ImageSharpFixed {
134 | base64: String
135 | tracedSVG: String
136 | aspectRatio: Float
137 | width: Float!
138 | height: Float!
139 | src: String!
140 | srcSet: String!
141 | srcWebp: String
142 | srcSetWebp: String
143 | originalName: String
144 | }
145 |
146 | type ImageSharpOriginal {
147 | height: Int!
148 | width: Int!
149 | src: String!
150 | }
151 | `
152 |
153 | const computeSharpSize = (source, info) => {
154 | let imageHeight = source.height
155 | let imageWidth = source.width
156 | let aspectRatio = imageWidth / imageHeight
157 |
158 | // Convert arguments to set of overrides
159 | let imageParams = {}
160 | for (let argument of info.field.arguments) {
161 | imageParams[argument.name?.value] = argument.value?.value
162 | }
163 |
164 | // Allow resizing in the browser
165 | if (imageParams.height && imageParams.width) {
166 | imageHeight = imageParams.height
167 | imageWidth = imageParams.width
168 | aspectRatio = imageWidth / imageHeight
169 | }
170 | // If one is set
171 | else if (imageParams.height) {
172 | imageHeight = imageParams.height
173 | imageWidth = imageParams.height * aspectRatio
174 | } else if (imageParams.width) {
175 | imageHeight = imageParams.width / aspectRatio
176 | imageWidth = imageParams.width
177 | }
178 | // Calculate based on max dimensions for fluid
179 | else if (imageParams.maxHeight) {
180 | imageHeight = imageParams.maxHeight
181 | imageWidth = imageParams.maxHeight * aspectRatio
182 | } else if (imageParams.maxWidth) {
183 | imageHeight = imageParams.maxWidth / aspectRatio
184 | imageWidth = imageParams.maxWidth
185 | }
186 |
187 | return {
188 | imageHeight: Number(imageHeight),
189 | imageWidth: Number(imageWidth),
190 | aspectRatio,
191 | }
192 | }
193 |
194 | const schemaExtensionResolvers = {
195 | ImageSharp: {
196 | fixed: (root, args, context, info) => {
197 | const source = root.fixed.parent
198 | const {
199 | imageWidth,
200 | imageHeight,
201 | aspectRatio,
202 | } = computeSharpSize(source, info)
203 | return {
204 | __typename: 'ImageSharpFixed',
205 | id: source.id,
206 | base64: '',
207 | tracedSVG: '',
208 | aspectRatio,
209 | width: imageWidth,
210 | height: imageHeight,
211 | src: source.src,
212 | srcSet: '',
213 | srcWebp: '',
214 | srcSetWebp: '',
215 | originalName: '',
216 | }
217 | },
218 | fluid: (root, args, context, info) => {
219 | const source = root.fluid.parent
220 | const {
221 | imageWidth,
222 | imageHeight,
223 | aspectRatio,
224 | } = computeSharpSize(source, info)
225 | return {
226 | __typename: 'ImageSharpFluid',
227 | id: source.id,
228 | base64: '',
229 | tracedSVG: '',
230 | aspectRatio,
231 | src: source.src,
232 | srcSet: '',
233 | srcWebp: null,
234 | srcSetWebp: null,
235 | sizes: '',
236 | originalImg: source.src,
237 | originalName: '',
238 | presentationWidth: imageWidth,
239 | presentationHeight: imageHeight,
240 | }
241 | },
242 | },
243 | CustomImage: {
244 | imageFile: (source, args, context, info) => {
245 | // Create a fake date
246 | const fileCreatedAt = new Date()
247 | const fileCreatedAtISO = fileCreatedAt.toISOString()
248 | const fileCreatedAtStamp = fileCreatedAt.getTime() / 1000
249 | // Seperare URL to get path, filename & extension
250 | const fileInfo = source.src
251 | .replace(/\\/g, '/')
252 | .match(/(.*\/)?(\..*?|.*?)(\.[^.]*?)?(#.*$|\?.*$|$)/)
253 | // Return a fake file instance
254 | return {
255 | __typename: 'File',
256 | sourceInstanceName: '__PROGRAMMATIC__',
257 | relativePath: source.src,
258 | absolutePath: source.src,
259 | changeTime: fileCreatedAtISO,
260 | size: 0,
261 | prettySize: '0 kB',
262 | accessTime: fileCreatedAtISO,
263 | atime: fileCreatedAtISO,
264 | atimeMs: fileCreatedAtStamp,
265 | base: fileInfo[2] + fileInfo[3],
266 | birthTime: fileCreatedAtISO,
267 | birthtimeMs: fileCreatedAtStamp,
268 | ctime: fileCreatedAtISO,
269 | ctimeMs: fileCreatedAtStamp,
270 | dir: fileInfo[1],
271 | ext: fileInfo[3],
272 | extension: fileInfo[3],
273 | id: source.id,
274 | publicURL: source.src,
275 | relativeDirectory: source.src,
276 | root: source.src,
277 | uid: source.id,
278 | url: source.src,
279 | childImageSharp: {
280 | __typename: 'ImageSharp',
281 | fluid: {
282 | __typename: 'ImageSharpFluid',
283 | parent: source,
284 | },
285 | fixed: {
286 | __typename: 'ImageSharpFixed',
287 | parent: source,
288 | },
289 | original: {
290 | __typename: 'ImageSharpOriginal',
291 | height: source.height,
292 | width: source.width,
293 | src: source.src,
294 | },
295 | },
296 | }
297 | },
298 | },
299 | }
300 |
301 | // Create Apollo client
302 | const fragmentMatcher = new IntrospectionFragmentMatcher({
303 | introspectionQueryResultData,
304 | })
305 | const cache = new InMemoryCache({ fragmentMatcher })
306 | const client = new ApolloClient({
307 | cache,
308 | link,
309 | typeDefs,
310 | resolvers: schemaExtensionResolvers,
311 | })
312 |
313 | if (content_type && token) {
314 | // Generate query from exported one in component
315 | const { query, subscriptionQuery } = generatePreviewQuery(
316 | isolatedQuery,
317 | content_type,
318 | token,
319 | fragments
320 | )
321 | // Get first version of preview to render the template
322 | client
323 | .query({ query: gql([query]) })
324 | .then((result) => onNext(result.data || {}))
325 | // Subscribe to any changes...
326 | client
327 | .subscribe({
328 | query: gql([subscriptionQuery]),
329 | variables: {},
330 | })
331 | .subscribe(
332 | (response) => onNext(response),
333 | (error) => console.log(error),
334 | (complete) => console.log(complete)
335 | )
336 | }
337 | }
338 |
339 | export const withPreview = (WrappedComponent, pageQuery, fragments = '') => {
340 | // ...and returns another component...
341 | return class extends React.Component {
342 | constructor(props) {
343 | super(props)
344 | this.state = {
345 | wagtail: cloneDeep(props.data ? props.data.wagtail : {}),
346 | }
347 | PreviewProvider(pageQuery, fragments, (data) => {
348 | this.setState({
349 | wagtail: merge({}, this.state.wagtail, data),
350 | })
351 | })
352 | }
353 |
354 | render() {
355 | const data = merge({}, this.props.data, this.state)
356 | if (data.wagtail.page) {
357 | return
358 | } else {
359 | return null
360 | }
361 | }
362 | }
363 | }
364 |
365 | const generatePreviewQuery = (query, contentType, token, fragments) => {
366 | // The preview args nessacery for preview backend to find the right model.
367 | query = cloneDeep(query)
368 | const previewArgs = [
369 | {
370 | kind: 'Argument',
371 | name: {
372 | kind: 'Name',
373 | value: 'contentType',
374 | },
375 | value: {
376 | block: false,
377 | kind: 'StringValue',
378 | value: contentType,
379 | },
380 | },
381 | {
382 | kind: 'Argument',
383 | name: {
384 | kind: 'Name',
385 | value: 'token',
386 | },
387 | value: {
388 | block: false,
389 | kind: 'StringValue',
390 | value: token,
391 | },
392 | },
393 | ]
394 |
395 | // Rename query for debugging reasons
396 | const queryDef = query.definitions[0]
397 | queryDef.arguments = []
398 | queryDef.variableDefinitions = []
399 |
400 | // Add field to AST
401 | const createSelection = (name) => ({
402 | kind: 'Field',
403 | name: {
404 | kind: 'Name',
405 | value: name,
406 | },
407 | arguments: [],
408 | directives: [],
409 | })
410 |
411 | // Alter the query so that we can execute it properly
412 | for (let node of traverse(query).nodes()) {
413 | // Get the node of any field attempting to download an image
414 | let imageFileNode = null
415 | if (
416 | node?.kind == 'Field' &&
417 | node?.selectionSet?.selections?.find(
418 | (selection) =>
419 | selection?.name?.value == 'imageFile' &&
420 | (imageFileNode = selection)
421 | )
422 | ) {
423 | // Make sure we have src, height & width details
424 | node.selectionSet.selections.push(createSelection('id'))
425 | node.selectionSet.selections.push(createSelection('src'))
426 | node.selectionSet.selections.push(createSelection('width'))
427 | node.selectionSet.selections.push(createSelection('height'))
428 |
429 | // Make sure it hit's the client side cache
430 | imageFileNode.directives.push({
431 | arguments: [],
432 | kind: 'Directive',
433 | name: {
434 | kind: 'Name',
435 | value: 'client',
436 | },
437 | })
438 |
439 | // Replace inline any fragments
440 | const fragmentTypes = require('gatsby-transformer-sharp/src/fragments.js')
441 | traverse(imageFileNode).map((node) => {
442 | if (
443 | node?.name?.value == 'fixed' ||
444 | node?.name?.value == 'fluid' ||
445 | node?.name?.value == 'original'
446 | ) {
447 | node.selectionSet.selections = node.selectionSet.selections
448 | .map((selection) => {
449 | Object.keys(fragmentTypes).map((fragmentName) => {
450 | if (selection?.name?.value == fragmentName) {
451 | const mod = fragmentTypes[fragmentName]
452 | const selections = gql([mod.source])
453 | selection =
454 | selections.definitions[0].selectionSet
455 | .selections
456 | }
457 | })
458 | return selection
459 | })
460 | .filter((selection) => !!selection)
461 | }
462 | })
463 |
464 | // Break as we don't need to visit any other nodes
465 | break
466 | }
467 | }
468 |
469 | if (queryDef.name) {
470 | queryDef.name.value = 'Preview' + queryDef.name.value
471 | } else {
472 | queryDef.name = {
473 | kind: 'Name',
474 | value: 'PreviewQuery',
475 | }
476 | }
477 |
478 | /*
479 | Iterate over fields on query and add preview args if it's a page.
480 | We store them as a var because if the query is a subscription we need to remove all
481 | non-page selections so we override the whole array with just the pages.
482 | */
483 | const pageSelections = queryDef.selectionSet.selections.filter(
484 | (selection) => {
485 | return selection.name.value.toLowerCase() === 'page'
486 | }
487 | )
488 | pageSelections.map((selection) => (selection.arguments = previewArgs))
489 |
490 | // Change query to subcription type
491 | const subscriptionQuery = cloneDeep(queryDef)
492 | subscriptionQuery.operation = 'subscription'
493 | subscriptionQuery.selectionSet.selections = pageSelections
494 |
495 | const updateFragments = (fragments) => {
496 | return fragments
497 | .replace('on ImageSharpFixed', 'on ImageSharpFixed @client')
498 | .replace('on ImageSharpFluid', 'on ImageSharpFluid @client')
499 | .replace('on ImageSharpOriginal', 'on ImageSharpOriginal @client')
500 | }
501 |
502 | return {
503 | query: `${updateFragments(fragments)} ${print(query)}`,
504 | subscriptionQuery: `${fragments} ${print(subscriptionQuery)}`,
505 | }
506 | }
507 |
508 | export const decodePreviewUrl = () => {
509 | if (typeof window !== 'undefined') {
510 | return qs.parse(window.location.search.slice(1))
511 | }
512 | return {}
513 | }
514 |
515 | export default withPreview
516 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | const { prepareOptions } = require(`gatsby/dist/utils/babel-loader-helpers`)
2 |
3 | exports.prepareOptions = (babel, options = {}, resolve = require.resolve) => {
4 | const items = prepareOptions(babel, options, resolve)
5 |
6 | if (items.length > 2) {
7 | items[3].splice(
8 | 0,
9 | 1,
10 | babel.createConfigItem(
11 | [
12 | require.resolve(
13 | 'babel-plugin-remove-graphql-queries'
14 | )
15 | ],
16 | {
17 | type: 'plugin'
18 | }
19 | )
20 | )
21 | }
22 |
23 | return items
24 | }
25 |
--------------------------------------------------------------------------------