├── packages
└── gatsby-groq-demo
│ ├── .prettierignore
│ ├── .prettierrc
│ ├── src
│ ├── data
│ │ ├── worlds
│ │ │ ├── ariel.json
│ │ │ ├── santo.json
│ │ │ ├── regina.json
│ │ │ ├── triumph.json
│ │ │ ├── higgins.json
│ │ │ ├── jiangyin.json
│ │ │ └── bellerophon.json
│ │ ├── characters
│ │ │ ├── triumphElder.json
│ │ │ ├── nandi.json
│ │ │ ├── saffron.json
│ │ │ ├── river.json
│ │ │ ├── simon.json
│ │ │ ├── wash.json
│ │ │ ├── inara.json
│ │ │ ├── jayne.json
│ │ │ ├── kaylee.json
│ │ │ ├── niska.json
│ │ │ ├── zoe.json
│ │ │ ├── malcom.json
│ │ │ └── shepherd.json
│ │ ├── ships
│ │ │ ├── skyplex.json
│ │ │ └── serenity.json
│ │ └── jobs
│ │ │ ├── spaceSalvage.json
│ │ │ ├── trainHeist.json
│ │ │ ├── triumphBandits.json
│ │ │ ├── hospital.json
│ │ │ ├── jaynestown.json
│ │ │ ├── heartOfGold.json
│ │ │ ├── cattle.json
│ │ │ └── lassiter.json
│ ├── images
│ │ ├── gatsby-icon.png
│ │ └── gatsby-astronaut.png
│ ├── pages
│ │ ├── 404.js
│ │ └── index.js
│ └── fragments
│ │ └── index.js
│ ├── gatsby-config.js
│ ├── LICENSE
│ └── package.json
├── utils.js
├── package.json
├── LICENSE
├── .gitignore
├── murmur.js
├── index.js
├── README.md
└── gatsby-node.js
/packages/gatsby-groq-demo/.prettierignore:
--------------------------------------------------------------------------------
1 | .cache
2 | package.json
3 | package-lock.json
4 | public
5 |
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "semi": false
4 | }
5 |
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/worlds/ariel.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "arielId",
3 | "name": "Ariel",
4 | "location": "Core"
5 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/worlds/santo.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "santoId",
3 | "name": "Santo",
4 | "location": "Core"
5 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/characters/triumphElder.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "triumphElderId",
3 | "position": "Village Elder"
4 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/worlds/regina.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "reginaId",
3 | "name": "Regina",
4 | "location": "Border"
5 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/worlds/triumph.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "triumphId",
3 | "name": "Triumph",
4 | "location": "Border"
5 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/worlds/higgins.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "higginsMoonId",
3 | "name": "Higgins",
4 | "location": "Border"
5 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/worlds/jiangyin.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "jiangyinId",
3 | "name": "Jiangyin",
4 | "location": "Border"
5 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/worlds/bellerophon.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "bellerpophonId",
3 | "name": "Bellerophon",
4 | "location": "Core"
5 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/images/gatsby-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kmcaloon/gatsby-plugin-groq/HEAD/packages/gatsby-groq-demo/src/images/gatsby-icon.png
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/characters/nandi.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "nandiId",
3 | "firstname": "Nandi",
4 | "lastname": "",
5 | "position": "Former Companion"
6 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/pages/404.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | const NotFoundPage = () => (
4 |
Lost
5 | )
6 |
7 | export default NotFoundPage
8 |
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/images/gatsby-astronaut.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kmcaloon/gatsby-plugin-groq/HEAD/packages/gatsby-groq-demo/src/images/gatsby-astronaut.png
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/characters/saffron.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "saffronId",
3 | "firstname": "Saffron",
4 | "lastname": "Unknown",
5 | "position": "Sneeky Crook"
6 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/characters/river.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "riverId",
3 | "firstname": "River",
4 | "lastname": "Tam",
5 | "position": "Assassin",
6 | "ship": {
7 | "_ref": "serenityId"
8 | }
9 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/characters/simon.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "simonId",
3 | "firstname": "Simon",
4 | "lastname": "Tam",
5 | "position": "Doctor",
6 | "ship": {
7 | "_ref": "serenityId"
8 | }
9 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/characters/wash.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "washId",
3 | "firstname": "Wash",
4 | "lastname": "Washburne",
5 | "position": "Pilot",
6 | "ship": {
7 | "_ref": "serenityId"
8 | }
9 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/characters/inara.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "inaraId",
3 | "firstname": "Inara",
4 | "lastname": "Serra",
5 | "position": "Companion",
6 | "ship": {
7 | "_ref": "serenityId"
8 | }
9 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/characters/jayne.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "jayneId",
3 | "firstname": "Jayne",
4 | "lastname": "Cobb",
5 | "position": "Hired Gun",
6 | "ship": {
7 | "_ref": "serenityId"
8 | }
9 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/characters/kaylee.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "kayleeId",
3 | "firstname": "Kaylee",
4 | "lastname": "Frye",
5 | "position": "Mechanic",
6 | "ship": {
7 | "_ref": "serenityId"
8 | }
9 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/characters/niska.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "niskaId",
3 | "firstname": "Adelai",
4 | "lastname": "Niska",
5 | "position": "Mob Boss",
6 | "ship": {
7 | "_ref": "skyplexId"
8 | }
9 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/characters/zoe.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "zoeId",
3 | "firstname": "Zoe",
4 | "lastname": "Washburne",
5 | "position": "First Mate",
6 | "ship": {
7 | "_ref": "serenityId"
8 | }
9 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/characters/malcom.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "malcomId",
3 | "firstname": "Malcom",
4 | "lastname": "Reynolds",
5 | "position": "Captain",
6 | "ship": {
7 | "_ref": "serenityId"
8 | }
9 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/characters/shepherd.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "shepherdId",
3 | "firstname": "Shepherd",
4 | "lastname": "Book",
5 | "position": "Shepherd",
6 | "ship": {
7 | "_ref": "serenityId"
8 | }
9 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/fragments/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Put all of your GROQ "fragments" here!
3 | */
4 |
5 | exports.getWorldsJobs = `
6 | * [ internal.type == "jobs" && ^.id in worlds[]._ref ] {
7 | client,
8 | crew,
9 | worlds,
10 | }
11 | `;
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/ships/skyplex.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "skyplexId",
3 | "name": "Niska's Skyplex",
4 | "type": "Space Station",
5 | "strengths": [
6 | "Heavily guarded"
7 | ],
8 | "weaknesses": [
9 | "Suprisingly easy to takeover"
10 | ]
11 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/jobs/spaceSalvage.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "spaceSalvageId",
3 | "worlds": [],
4 | "client": {
5 | "_ref": "badgerId"
6 | },
7 | "crew": [
8 | { "_ref": "malcomId" },
9 | { "_ref": "jayneId" },
10 | { "_ref": "washId" }
11 | ]
12 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/ships/serenity.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "serenityId",
3 | "name": "Serenity",
4 | "type": "Firefly",
5 | "strengths": [
6 | "Large cargo bay",
7 | "Plenty of hiding spots"
8 | ],
9 | "weaknesses": [
10 | "Compression coils"
11 | ]
12 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/jobs/trainHeist.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "trainHeistId",
3 | "worlds": [
4 | { "_ref": "reginaId" }
5 | ],
6 | "client": {
7 | "_ref": "niskaId"
8 | },
9 | "crew": [
10 | { "_ref": "malcomId" },
11 | { "_ref": "jayneId" },
12 | { "_ref": "zoeId" }
13 | ]
14 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/jobs/triumphBandits.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "triumphBanditsId",
3 | "worlds": [
4 | { "_ref": "triumphId" }
5 | ],
6 | "client": {
7 | "_ref": "niskaId"
8 | },
9 | "crew": [
10 | { "_ref": "malcomId" },
11 | { "_ref": "jayneId" },
12 | { "_ref": "zoeId" }
13 | ]
14 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/jobs/hospital.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "hospitalId",
3 | "worlds": [
4 | { "_ref": "aerialId" }
5 | ],
6 | "client": {
7 | "_ref": "simonId"
8 | },
9 | "crew": [
10 | { "_ref": "malcomId" },
11 | { "_ref": "zoeId" },
12 | { "_ref": "jayneId" },
13 | { "_ref": "simonId" }
14 | ]
15 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/jobs/jaynestown.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "jaynestownId",
3 | "worlds": [
4 | { "_ref": "higginsMoonId" }
5 | ],
6 | "client": {
7 | "_ref": "?"
8 | },
9 | "crew": [
10 | { "_ref": "malcomId" },
11 | { "_ref": "jayneId" },
12 | { "_ref": "kayleeId" },
13 | { "_ref": "washId" },
14 | { "_ref": "simonId" }
15 | ]
16 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/jobs/heartOfGold.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "heartOfGoldId",
3 | "client": {
4 | "_ref": "nandiId"
5 | },
6 | "crew": [
7 | { "_ref": "malcomId" },
8 | { "_ref": "zoeId" },
9 | { "_ref": "jayneId" },
10 | { "_ref": "washId" },
11 | { "_ref": "shepherdId" },
12 | { "_ref": "simonId" },
13 | { "_ref": "inaraId" }
14 | ]
15 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/jobs/cattle.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "cattleId",
3 | "worlds": [
4 | { "_ref": "santoId" },
5 | { "_ref": "jiangyinId" }
6 | ],
7 | "client": {
8 | "_ref": "badgerId"
9 | },
10 | "crew": [
11 | { "_ref": "malcomId" },
12 | { "_ref": "jayneId" },
13 | { "_ref": "kayleeId" },
14 | { "_ref": "shepherdId" },
15 | { "_ref": "zoeId" }
16 | ]
17 | }
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 | const chalk = require( 'chalk' );
2 |
3 | /**
4 | * Custom reporter.
5 | */
6 | exports.reporter = {
7 | info: msg => console.log( `${chalk.blue( 'info' )} [groq] ${msg}` ),
8 | warn: msg => console.warn( `[groq] ${msg}` ),
9 | success: msg => console.log( `${chalk.green( 'success' )} [groq] ${msg}` ),
10 | error: msg => console.log( `${chalk.hex( '#730202' ).bold( `${msg} \n` ) }` ),
11 | }
12 |
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/data/jobs/lassiter.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "lassiterId",
3 | "worlds": [
4 | { "_ref": "bellerophonId" }
5 | ],
6 | "client": {
7 | "_ref": "saffronId"
8 | },
9 | "crew": [
10 | { "_ref": "malcomId" },
11 | { "_ref": "saffronId" },
12 | { "_ref": "kayleeId" },
13 | { "_ref": "zoeId" },
14 | { "_ref": "jayneId" },
15 | { "_ref": "washId" },
16 | { "_ref": "inaraId" }
17 | ]
18 | }
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/gatsby-config.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Set up json transformers and
4 | * configure groq plugin.
5 | */
6 | module.exports = {
7 | plugins: [
8 | {
9 | resolve: 'gatsby-plugin-groq',
10 | options: {
11 | // Change this if you change the fragments index.
12 | fragmentsDir: './src/fragments',
13 | }
14 | },
15 | {
16 | resolve: 'gatsby-transformer-json',
17 | options: {
18 | typeName: ( { node, object, isArray } ) => node.relativeDirectory,
19 | }
20 | },
21 | {
22 | resolve: 'gatsby-source-filesystem',
23 | options: {
24 | path: './src/data/',
25 | }
26 | },
27 | ],
28 | }
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby-plugin-groq",
3 | "description": "Gatsby plugin for using GROQ in place of GraphQL",
4 | "version": "1.0.0-alpha.15",
5 | "author": "Kevin McAloon ",
6 | "keywords": [
7 | "gatsby"
8 | ],
9 | "license": "MIT",
10 | "homepage": "https://github.com/kmcaloon/gatsby-plugin-groq",
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/kmcaloon/gatsby-plugin-groq"
14 | },
15 | "dependencies": {
16 | "@babel/parser": "^7.9.6",
17 | "@babel/traverse": "^7.9.6",
18 | "axios": "^0.19.2",
19 | "chokidar": "^3.4.0",
20 | "fs": "^0.0.1-security",
21 | "glob": "^7.1.6",
22 | "groq-js": "0.1.5",
23 | "normalize-path": "^3.0.0",
24 | "string.prototype.matchall": "^4.0.2"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useGroqQuery } from '../../plugins/gatsby-plugin-groq';
3 |
4 | import { getWorldsJobs } from '../fragments';
5 |
6 |
7 |
8 | export const groqQuery = `{
9 | "worlds": *[ internal.type == "worlds" ] {
10 | _id,
11 | name,
12 | "jobs": ${getWorldsJobs}
13 | }
14 | }`;
15 | const IndexPage = ( { pageContext } ) => {
16 |
17 | const { data } = pageContext;
18 | const awesomeCharacters = useGroqQuery( `
19 | *[ internal.type == "characters" ] {
20 | ...
21 | }
22 | ` );
23 |
24 | console.log( 'Worlds', data.worlds );
25 | console.log( 'Awesome Characters', awesomeCharacters );
26 |
27 | return(
28 |
29 |
30 |
Try adding a groqQuery export to this page!
31 |
32 |
33 | )
34 |
35 | }
36 |
37 | export default IndexPage
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 gatsbyjs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 gatsbyjs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/packages/gatsby-groq-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby-groq-demo",
3 | "private": true,
4 | "description": "A demo to play around with gatsby-plugin-groq",
5 | "version": "0.1.0",
6 | "author": "Kevin McAloon ",
7 | "dependencies": {
8 | "@babel/parser": "^7.9.6",
9 | "@babel/traverse": "^7.9.6",
10 | "axios": "^0.19.2",
11 | "chokidar": "^3.4.0",
12 | "fs": "^0.0.1-security",
13 | "gatsby": "^2.21.21",
14 | "gatsby-source-filesystem": "^2.3.1",
15 | "gatsby-transformer-json": "^2.4.1",
16 | "glob": "^7.1.6",
17 | "groq-js": "^0.1.5",
18 | "normalize-path": "^3.0.0",
19 | "prop-types": "^15.7.2",
20 | "react": "^16.12.0",
21 | "react-dom": "^16.13.1"
22 | },
23 | "devDependencies": {
24 | "prettier": "2.0.5"
25 | },
26 | "keywords": [
27 | "gatsby"
28 | ],
29 | "license": "MIT",
30 | "scripts": {
31 | "build": "gatsby build",
32 | "develop": "gatsby develop",
33 | "format": "prettier --write \"**/*.{js,jsx,json,md}\"",
34 | "start": "gatsby develop",
35 | "serve": "gatsby serve",
36 | "clean": "gatsby clean",
37 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # dotenv environment variable files
55 | .env*
56 |
57 | # gatsby files
58 | .cache/
59 | public
60 |
61 | # Mac files
62 | .DS_Store
63 |
64 | # Yarn
65 | yarn-error.log
66 | .pnp/
67 | .pnp.js
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | *.lock
72 |
--------------------------------------------------------------------------------
/murmur.js:
--------------------------------------------------------------------------------
1 | // murmurhash2 via https://gist.github.com/raycmorgan/588423
2 |
3 | module.exports = ( str, seed = 'abc' ) => {
4 |
5 | if( ! str ) {
6 | return null;
7 | }
8 |
9 | let m = 0x5bd1e995
10 | let r = 24
11 | let h = seed ^ str.length
12 | let length = str.length
13 | let currentIndex = 0
14 |
15 | while (length >= 4) {
16 | let k = UInt32(str, currentIndex)
17 |
18 | k = Umul32(k, m)
19 | k ^= k >>> r
20 | k = Umul32(k, m)
21 |
22 | h = Umul32(h, m)
23 | h ^= k
24 |
25 | currentIndex += 4
26 | length -= 4
27 | }
28 |
29 | switch (length) {
30 | case 3:
31 | h ^= UInt16(str, currentIndex)
32 | h ^= str.charCodeAt(currentIndex + 2) << 16
33 | h = Umul32(h, m)
34 | break
35 |
36 | case 2:
37 | h ^= UInt16(str, currentIndex)
38 | h = Umul32(h, m)
39 | break
40 |
41 | case 1:
42 | h ^= str.charCodeAt(currentIndex)
43 | h = Umul32(h, m)
44 | break
45 | }
46 |
47 | h ^= h >>> 13
48 | h = Umul32(h, m)
49 | h ^= h >>> 15
50 |
51 | return h >>> 0
52 | }
53 |
54 | function UInt32(str, pos) {
55 | return (
56 | str.charCodeAt(pos++) +
57 | (str.charCodeAt(pos++) << 8) +
58 | (str.charCodeAt(pos++) << 16) +
59 | (str.charCodeAt(pos) << 24)
60 | )
61 | }
62 |
63 | function UInt16(str, pos) {
64 | return str.charCodeAt(pos++) + (str.charCodeAt(pos++) << 8)
65 | }
66 |
67 | function Umul32(n, m) {
68 | n = n | 0
69 | m = m | 0
70 | let nlo = n & 0xffff
71 | let nhi = n >>> 16
72 | let res = (nlo * m + (((nhi * m) & 0xffff) << 16)) | 0
73 | return res
74 | }
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const groq = require( 'groq-js' );
2 | const matchAll = require( 'string.prototype.matchall' );
3 | const murmurhash = require( './murmur' );
4 | const path = require( 'path' );
5 | const { reporter } = require( './utils' );
6 |
7 | const ROOT = path.resolve( __dirname, '../..' );
8 | const GROQ_DIR = process.env.NODE_ENV === 'development' ? `${ROOT}/.cache/groq` : `${ROOT}/public/static/groq`;
9 |
10 |
11 | /**
12 | * Use directory settings throughout plugin.
13 | */
14 | exports.groqDirectories = { ROOT, GROQ_DIR };
15 |
16 |
17 | /**
18 | * Hook to mimic Gatsby's static query.
19 | * During extraction the plugin fines and extracts these queries
20 | * and stores them in a directory. During SSR and runtime this function
21 | * fetches the query reults from wherever they are being cached.
22 | *
23 | * @param {string} query
24 | * @return {array}
25 | */
26 | exports.useGroqQuery = query => {
27 |
28 | const hash = murmurhash( query );
29 |
30 | try {
31 | const result = require( `${process.env.GROQ_DIR}/${hash}.json` );
32 | return result;
33 | }
34 | catch( err ) {
35 | console.warn( err );
36 | }
37 |
38 | }
39 |
40 | /**
41 | * Groq query helper function.
42 | *
43 | * @param {string} rawQuery
44 | * @param {map} dataset
45 | * @param {Object} options
46 | * @param {Object} options.fragments
47 | * @param {Object} options.params
48 | * @param {string} options.file For debugging.
49 | * @return {Object} Array of results along with final query and query string to get hashed.
50 | */
51 | exports.runQuery = async ( rawQuery, dataset, options = {} ) => {
52 |
53 | const { file, fragments, params } = options;
54 | let query = rawQuery;
55 |
56 | // Check if query has fragment.
57 | const hasFragment = query.includes( '${' );
58 |
59 | if( hasFragment ) {
60 | query = processFragments( query, fragments );
61 | }
62 |
63 | const queryToHash = query;
64 |
65 | query = processJoins( query );
66 |
67 | try {
68 |
69 | const strippedQuery = query.replace( /`/g, '', );
70 | const parsedQuery = groq.parse( strippedQuery );
71 | const value = await groq.evaluate( parsedQuery, { dataset } );
72 | const result = await value.get();
73 |
74 | return { result, finalQuery: query, queryToHash }
75 |
76 | }
77 | catch( err ) {
78 | console.error( file );
79 | reporter.error( `${err}` );
80 | reporter.error( `Query: ${query}` );
81 |
82 | return err;
83 |
84 | }
85 |
86 |
87 | }
88 |
89 | /**
90 | * Process joins.
91 | *
92 | * @param {string} query
93 | * @return {string}
94 | */
95 | function processJoins( query ) {
96 |
97 | // We need to figure out a clean way to get plugin options...
98 | let processedQuery = query;
99 |
100 | if( processedQuery.includes( '->' ) ) {
101 |
102 | const optionsDir = process.env.GROQ_DIR || GROQ_DIR;
103 | const { autoRefs, referenceMatcher } = require( `${optionsDir}/options` );
104 | const matchField = referenceMatcher || 'id';
105 | const refOption = !! autoRefs ? '._ref' : '';
106 |
107 | const search = `\\S+->\\w*`;
108 | const regex = new RegExp( search, 'g' );
109 | const matches = [ ... matchAll( processedQuery, regex ) ];
110 |
111 | if( !! matches.length ) {
112 | for( let match of matches ) {
113 |
114 | const matchText = match[0];
115 |
116 | // For now we're skipping Sanity .assets since they work by default.
117 | if( matchText.includes( '.asset->' ) ) {
118 | continue;
119 | }
120 |
121 | const field = matchText.replace( '->', '' );
122 | let replace = null;
123 |
124 | // Single refs.
125 | if( ! field.includes( '[]' ) ) {
126 | replace = `*[ ${matchField} == ^.${field}${refOption} ][0]`;
127 | }
128 | // Arrays.
129 | else {
130 | replace = `*[ ${matchField} in ^.${field}${refOption} ]`;
131 | }
132 |
133 | processedQuery = processedQuery.replace( matchText, replace );
134 |
135 | }
136 |
137 | }
138 | }
139 |
140 | return processedQuery;
141 |
142 | }
143 |
144 | /**
145 | * Process fragments.
146 | *
147 | * @param {string} query
148 | * @param {object} fragments
149 | * @return {string}
150 | */
151 | function processFragments( query, fragments ) {
152 |
153 | let processedQuery = query;
154 |
155 | if( ! fragments || ! Object.keys( fragments ).length ) {
156 | reporter.warn( 'Query contains fragments but no index provided.' );
157 | return null;
158 | }
159 |
160 | // For now we are just going through all fragments and running
161 | // simple string replacement.
162 | for( let [ name, value ] of Object.entries( fragments ) ) {
163 |
164 | if( ! processedQuery.includes( name ) ) {
165 | continue;
166 | }
167 |
168 | // Process string.
169 | if( typeof value === 'string' ) {
170 | const search = `\\$\\{(${name})\\}`;
171 | const pattern = new RegExp( search, 'g' );
172 | processedQuery = processedQuery.replace( pattern, value );
173 | }
174 | // Process function.
175 | // else if( typeof value === 'function' ) {
176 | //
177 | // }
178 |
179 | }
180 |
181 | return processedQuery;
182 |
183 |
184 | }
185 |
186 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # gatbsy-plugin-groq
2 |
3 | **Gatsby plugin for using GROQ in place of GraphQL**
4 |
5 | The purpose of this plugin is to merge the power of GROQ and Gatsby by allowing developers to run GROQ queries against Gatsby's data layer in their page and static queries. For those of you who are familiar with GROQ, you are probably already in love and need no introduction. For everyone else, I highly suggest reading the below [What is This](#introduction) and [Resources](#resources) sections.
6 |
7 | Included in this repository is a demo Gatsby starter with some data to play around with. You can find it under `packages/gatsby-groq-starter`. Just download the files and follow the plugin installation instructions below to start having fun.
8 |
9 | **View low quality demo here:**
10 | https://drive.google.com/file/d/1FVch2HbAWk1TEXph1katiNb1uuaBLSHf/view?usp=sharing
11 |
12 | ## 🎂 Features
13 | - Works with any data pulled into Gatsby's data layer
14 | - Replicates Gatsby's beloved patterns
15 | - GROQ-based page queries with HMR
16 | - GROQ-based static queries with live reloads
17 | - Leverages GROQ's native functionality for advanced querying, node/document projections, joins (see [notes on joins](#joins)), etc,
18 | - String interpolation ("fragments") within queries, much more flexible than GraphQL fragments
19 | - GROQ explorer in browser during development at `locahost:8000/__groq` **(TO DO)**
20 | - Optimized for incremental builds on Cloud and OSS **(TO DO)**
21 |
22 | ## 🚀 Get Started
23 |
24 | 1. At the root of your Gatsby project, install from the command line:
25 | ```
26 | npm install --save gatsby-plugin-groq
27 | // or
28 | yarn add gatsby-plugin-groq
29 | ```
30 | 2. In `gatbsy-config.js`, add the plugin configuration to the `plugins` array:
31 | ```
32 | module.exports = {
33 | //...
34 | plugins: [
35 | {
36 | resolve: 'gatsby-plugin-groq',
37 | options: {
38 | // Location of your project's fragments index file.
39 | // Only required if you are implementing fragments. Defaults to `./src/fragments`.
40 | fragmentsDir: './src/fragments',
41 | // Determines which field to match when using joins. Defaults to `id`.
42 | referenceMatcher: 'id',
43 | // If only using Sanity documents, change this to true to use default method
44 | // of joining documents without having to append ._ref to the referencing field.
45 | // Defaults to false.
46 | autoRefs: false,
47 | }
48 | }
49 | ]
50 | }
51 | ```
52 | 3. To use a GROQ page query, simply add a named `groqQuery` export to the top of your component file as you would a Gatsby query:
53 | ```
54 | export const groqQuery = `
55 | ...
56 | `
57 | ```
58 | 4. To use a GROQ static query, use the `useGroqQuery` hook:
59 | ```
60 | import { useGroqQuery } from 'gatsby-plugin-groq';
61 |
62 | export function() {
63 |
64 | const data = useGroqQuery( `
65 | ...
66 | ` );
67 |
68 | }
69 | ```
70 | 5. For more flexibility and advanced usage check out [Fragments](#fragments)
71 |
72 | **NOTE: If using joins or the Sanity source plugin, please see [limitations](#limitations)**
73 |
74 | ## 🤔 What is This?
75 | Gatsby is an amazing tool that has helped advance modern web development in significant ways. While many love it for its magical frontend concoction of static generation and rehydration via React, easy routing, smart prefetching, image rendering, etc., one of the key areas where it stands out from other similar tools is its GraphQL data layer. This feature is a large part of why some developers love Gatsby and why others choose to go in another direction. Being able to source data from multiple APIs, files, etc. and compile them altogether into a queryable GraphQL layer is ***amazing***, but many developers simply don't enjoy working with GraphQL. This is where GROQ comes in.
76 |
77 | GROQ (**G**raph-**R**elational **O**bject **Q**ueries) is an incredibly robust and clear general query language designed by the folks at [Sanity](https://www.sanity.io/) for filtering and projecting JSON data. In many ways it is very similar to GraphQL in that you can run multiple robust queries and specify the data you need all within a single request, however with GROQ you can accomplish much more in a smoother and more flexible way. It supports complex parameters and operators, functions, piping, advanced joins, slicing, ordering, projections, conditionals, pagination etc., all with an intuitive syntax 😲.
78 |
79 | For example, take this somewhat simple GraphQL query:
80 |
81 | ```
82 | {
83 | authors(where: {
84 | debutedBefore_lt: "1900-01-01T00:00:00Z",
85 | name_matches: "Edga*r"
86 | ) {
87 | name,
88 | debutYear,
89 | }
90 | }
91 | ```
92 |
93 | Here is what it would look like using GROQ:
94 |
95 | ```
96 | *[_type == "author" && name match "Edgar" && debutYear < 1900]{
97 | name,
98 | debutYear
99 | }
100 | ```
101 |
102 | The more complex the queries, the more GROQ's advantages shine. This is why some developers already familiar with GROQ bypass Gatsby's data layer so that they could leverage its power.
103 |
104 |
105 | ## 🧙 How it Works
106 | This plugin mimics Gatsby's own method of extracting queries from components by using a few Babel tools to parse files and traverse code to capture all queries found in files. By leveraging Gatsby's Node APIs and helpers we are able to extract queries from files on demand then run them against all GraphQL nodes found in Gatsby's redux store. After queries are run we can either feed results into a page's context (page queries), or cache for later use within individual components (static queries). Everything was done to leverage available APIs and to avoid interacting with Gatsby's data store directly as much as possible.
107 |
108 | For now, all cache related to groq queries can be found in `.cache/groq` during development and `public/static/groq` in production.
109 |
110 | ### Page Queries
111 | All page-level components with `groqQuery` exports will have their queries (template literal) extracted and cached unprocessed as a hashed json file in the groq directory. The hash is based on the component file's full path so that the cached file can always be associated with the component. During bootstrap, whenever a page is created via `createPage` the plugin checks the cache to see if there is a page query related to it page's component. If there is, it then runs the query and replaces any variables with values supplied in the page's context. The result is stored in the `data` property within `pageContext`.
112 |
113 | For example, if a page query contains `*[ _type == "page" && _id == $_id ]{ ... }` and a page is created with `context: { _id: "123" }`, the plugin will include the variable and run the query: `*[ _type == "page" && _id == "123" ]{ ... }`.
114 |
115 | During development all files are watched for changes. Whenever there is a change to a file containing a page query the above process runs again and the updated data is injected into all paths that contain the changed component.
116 |
117 | ### Static Queries
118 | All components using the hook `useGroqQuery` first have these queries extracted, processed, and cached during bootstrap. However unlike page queries, static query hashes are based off of the queries themselves and contain the actual results of their queries after they have been run. If a static query changes, it generates a new hashed result. During SSR and runtime, the `useGroqQuery` function then retrieves the cache associated to its query and returns the result.
119 |
120 | Similar to page queries, all files are watched for changes and whenever there is a change to a file containing a static query the above process runs again, the query results are cached, and the page refreshes with the static query now returning the updated content.
121 |
122 | ### Fragments
123 | Playing off of GraphQL, "fragments" are strings of reusable portions of GROQ queries that can be interpolated within other queries. For example, say you have a blog where you are showing post snippets throughout multiple page templates and for every post need to retrieve its `id`, `title`, `summary`, and `category`, along with the category's `name` and `slug`. Instead of having to remember which fields you need and write this out every time, you could create a reusable fragment:
124 |
125 | ```
126 | exports.postSnippetFields = `
127 | id,
128 | summary,
129 | title,
130 | "category": *[ type == "category" && id == ^.category ] {
131 | name,
132 | slug
133 | }
134 | `
135 | ```
136 |
137 | Then simply reuse the fragment wherever you need:
138 |
139 | ```
140 | import { postSnippetFields } from 'src/fragments';
141 |
142 | const groqQuery = `
143 | *[ type == "post" ] {
144 | ${postSnippetFields}
145 | }
146 | ```
147 | To use GROQ fragments with this plugin, for now all fragments must be exported from a `index.js` using CommonJS syntax. You must also specify the directory where this file is found within the plugin options: `fragmentsDir: // Directory relative to project root`.
148 |
149 | **That should cover most of it. Check the comments within code for more details.**
150 |
151 | ## 🤦 Limitations
152 | ### Joins
153 | The ability to join multiple documents and retrieve their fields is a popular feature of GROQ. More testing needs to be done, but currently most join syntaxes are supported other than the `references()` function.
154 |
155 | For Sanity users, if using the `->` operator for everything other than file assets (i.e. images) you will need to append `._ref` to the field which contains the reference. So with a Sanity dataset, the usual
156 | `referenceField->{ ... }` would instead look like this: `referenceField._ref->{ ... }`. Likewise for arrays: `arrayOfReferences[]._ref->{ ... }`. If you are only using Sanity data within your Gatsby project and would like to use the regular syntax, within the plugin options set `autoRefs: true` and you won't have to worry about appending the extra field.
157 |
158 | ### Other usage with gatsby-source-sanity
159 | For every `_ref` field within your documents the source plugin injects the referenced document's GraphQL node id instead of its default `_id` value. This means that whenever you are trying to match `_ref` values to documents you need to use the `id` field instead of `_id`.
160 |
161 | So instead of what you are used to:
162 | ```
163 | {
164 | ...,
165 | "document: *[ _id == ^._ref ] {
166 | ...
167 | }[0]
168 | }
169 | ```
170 | You would use this:
171 | ```
172 | {
173 | ...,
174 | "document: *[ id == ^._ref ] {
175 | ...
176 | }[0]
177 | }
178 | ```
179 | If you are using the source plugin and running issues into issues with your joins, if all else fails try double checking your queries and make sure you are matching every `_ref` to the actual node `id`.
180 |
181 | ## ⌛ TO DO (random order)
182 | - ~~Get rid of relative directories~~
183 | - ~~Work on issues with joins~~ we might be limited here
184 | - ~~Clean up spotty caching issues after running development~~
185 | - ~~Experiment with other data sources (WordPress)~~
186 | - GROQ explorer
187 | - Allow for fragment functions
188 | - Set up page refreshing when fragments are changed
189 | - Look into using esm for ES6 imports/exports
190 | - Set up an option to auto-resolve references?
191 | - Error messaging (especially when there are Babel parsing errors)
192 | - Performance optimizations
193 | - Improve docs
194 | - Provide recipe docs with heavy use of fragments
195 | - Incremental builds?
196 | - Allow for variables within static queries?
197 | - Helpers for real-time listening in client (Sanity only)
198 | - Tests
199 | - Proselytize everyone from GraphQL to GROQ.
200 |
201 | ## 📖 GROQ Resources
202 | - [GROQ Intro Video](https://www.youtube.com/watch?v=Jcfubj2zRI0)
203 | - [GROQ Docs](https://www.sanity.io/docs/overview-groq)
204 | - [CSS Ticks - The Best (GraphQL) API is One You Write](https://css-tricks.com/the-best-graphql-api-is-one-you-write/)
205 | - [Review of GROQ, A New JSON Query Language](https://nordicapis.com/review-of-groq-a-new-json-query-language/)
206 |
207 | ## 🙇 Huge Thanks
208 | Thanks to the awesome teams at [Gatsby](https://www.gatsbyjs.org/) and [Sanity](https://www.sanity.io/) for their absolutely amazing tools and developer support. If you haven't checked it out yet, I would **HIGHLY** recommend looking into Sanity's incredible CMS. It's hard to imagine how a headless CMS experience could be any better.
209 |
--------------------------------------------------------------------------------
/gatsby-node.js:
--------------------------------------------------------------------------------
1 | const fs = require( 'fs' );
2 | const glob = require( 'glob' );
3 | const murmurhash = require( './murmur' );
4 | const normalizePath = require( 'normalize-path' );
5 | const parser = require( '@babel/parser' );
6 | const path = require( 'path' );
7 | const traverse = require( '@babel/traverse' ).default;
8 | const { watch } = require( 'chokidar' );
9 | const { runQuery } = require( './index' );
10 | const { reporter } = require( './utils' );
11 | const { groqDirectories } = require( './index' );
12 |
13 | const { GROQ_DIR, ROOT } = groqDirectories;
14 |
15 |
16 | /**
17 | * Here's where we extract and run all initial queries.
18 | * Also sets up a watcher to re-run queries during dev when files change.fcache
19 | *
20 | * Runs in right after schema creation and before createPages.
21 | */
22 | exports.resolvableExtensions = async ( { graphql, actions, cache, getNodes, traceId, store }, plugin ) => {
23 |
24 | if( !! fs.existsSync( GROQ_DIR ) ) {
25 | fs.rmdirSync( GROQ_DIR, { recursive: true } );
26 | }
27 | fs.mkdirSync( GROQ_DIR );
28 |
29 | // Cache options (because we need them on frontend)
30 | // Probably a more sophisticated Gatsby way of doing this.
31 | if( !! plugin ) {
32 |
33 | fs.writeFileSync( `${GROQ_DIR}/options.json`, JSON.stringify( plugin ), err => {
34 | if( err ) {
35 | throw new Error( err );
36 | }
37 | } );
38 |
39 | }
40 |
41 | // Cache fragments.
42 | const fragmentsDir = !! plugin.fragmentsDir ? path.join( ROOT, plugin.fragmentsDir ) : null;
43 |
44 | if( !! fragmentsDir ) {
45 | cacheFragments( fragmentsDir, cache );
46 | }
47 |
48 | // Extract initial queries.
49 | const intitialNodes = getNodes();
50 |
51 | extractQueries( { nodes: intitialNodes, traceId, cache } );
52 |
53 |
54 | // For now watching all files to re-extract queries.
55 | // Right now there doesn't seem to be a way to watch for build updates using Gatsby's public node apis.
56 | // Created a ticket in github to explore option here.
57 | const watcher = watch( `${ROOT}/src/**/*.js` );
58 |
59 | watcher.on( 'change', async ( filePath ) => {
60 |
61 | // Recache if this was a change within fragments directory.
62 | if( !! fragmentsDir && filePath.includes( fragmentsDir ) ) {
63 |
64 | await cacheFragments( fragmentsDir, cache );
65 |
66 | //TODO need to figure out a way to refresh page with new data.
67 | reporter.info( `DON'T FORGET TO RESAVE FILES WITH UPDATED FRAGMENT!` );
68 |
69 | }
70 |
71 | // Get info for file that was changed.
72 | const fileContents = fs.readFileSync( filePath, 'utf8' );
73 |
74 | // Check if file has either page or static queries.
75 | const pageQueryMatch = /export const groqQuery = /.exec( fileContents );
76 | const staticQueryMatch = /useGroqQuery/.exec( fileContents );
77 | if( ! pageQueryMatch && ! staticQueryMatch ) {
78 | return;
79 | }
80 |
81 | reporter.info( 'Re-processing groq queries...' );
82 |
83 | // Get updated nodes to query against.
84 | const nodes = getNodes();
85 |
86 | // Run page queries.
87 | if( pageQueryMatch ) {
88 |
89 | const { deletePage, createPage } = actions;
90 | const { pages } = store.getState();
91 |
92 | // First we need to reprocess the page query.
93 | const processedPageQuery = await processFilePageQuery( filePath, nodes, cache );
94 |
95 | if( !! processedPageQuery ) {
96 |
97 | const { fileHash: newHash, query: newQuery } = processedPageQuery;
98 | const queryFile = `${GROQ_DIR}/${newHash}.json`;
99 |
100 | await cacheQueryResults( newHash, newQuery );
101 |
102 | // Update all paths using this page component.
103 | // Is this performant or should we try to leverage custom cache?
104 | for( let [ path, page ] of pages ) {
105 |
106 | if( page.component !== filePath ) {
107 | continue;
108 | }
109 |
110 | reporter.info( `Updating path: ${page.path}` );
111 |
112 | // Run query and inject into page context.
113 | pageQueryToContext( {
114 | actions,
115 | cache,
116 | file: queryFile,
117 | nodes,
118 | page
119 | } );
120 |
121 | }
122 |
123 | }
124 |
125 | }
126 |
127 | // Static queries.
128 | if( ! staticQueryMatch ) {
129 | return reporter.success( 'Finished re-processing page queries' );
130 | }
131 |
132 | // Run query and save to cache.
133 | // Files using the static query will be automatically refreshed.
134 | const staticQueries = await processFileStaticQueries( filePath, nodes, cache );
135 |
136 | if( !! ( staticQueries || [] ).length ) {
137 |
138 | for( let { hash, json } of staticQueries ) {
139 |
140 | if( ! hash || ! json ) {
141 | return reporter.warn( 'There was a problem processing one of your static queries' );
142 | }
143 |
144 | cacheQueryResults( hash, json );
145 |
146 | }
147 |
148 | }
149 |
150 | return reporter.success( 'Finished re-processing queries' );
151 |
152 |
153 | } );
154 |
155 | };
156 |
157 | /**
158 | * Inject page query results its page.
159 | */
160 | exports.onCreatePage = async ( { actions, cache, getNodes, page, traceId } ) => {
161 |
162 | // Check for hashed page queries for this component.
163 | const componentPath = page.component;
164 | const hash = murmurhash( componentPath );
165 | const queryFile = `${GROQ_DIR}/${hash}.json`;
166 |
167 | if( ! fs.existsSync( queryFile) ) {
168 | return;
169 | }
170 |
171 | // Run query and write to page context.
172 | pageQueryToContext( {
173 | actions,
174 | cache,
175 | file: queryFile,
176 | getNodes,
177 | page,
178 | } );
179 |
180 |
181 | }
182 |
183 | /**
184 | * Extract page and static queries from all files.
185 | * Process and cache results.
186 | *
187 | * @param {Object} $0 Gatsby Node Helpers.
188 | */
189 | async function extractQueries( { nodes, traceId, cache } ) {
190 |
191 | reporter.info( 'Getting files for groq extraction...' );
192 |
193 | const filesRegex = `*.+(t|j)s?(x)`
194 | const pathRegex = `/{${filesRegex},!(node_modules)/**/${filesRegex}}`;
195 | let hasErrors = false;
196 | let files = [
197 | path.join( ROOT, 'src' ),
198 | ].reduce( ( merged, folderPath ) => {
199 |
200 | merged.push(
201 | ...glob.sync( path.join( folderPath, pathRegex ), {
202 | nodir: true,
203 | } )
204 | );
205 |
206 | return merged;
207 |
208 | }, [] );
209 |
210 | files = files.filter( d => ! d.match( /\.d\.ts$/ ) );
211 | files = files.map( normalizePath );
212 |
213 | // Loop through files and look for queries to extract.
214 | for( let file of files ) {
215 |
216 | const pageQuery = await processFilePageQuery( file, nodes, cache );
217 | const staticQueries = await processFileStaticQueries( file, nodes, cache );
218 |
219 | // Cache page query.
220 | // This will only contain a json file of unprocessed query.
221 | if( !! pageQuery ) {
222 | const { fileHash, query } = pageQuery;
223 | cacheQueryResults( fileHash, query );
224 | }
225 |
226 | // Cache static queries.
227 | // These will contain actual results of the query.
228 | if( !! ( staticQueries || [] ).length ) {
229 |
230 | for( let { hash, json } of staticQueries ) {
231 |
232 | if( ! hash || ! json ) {
233 | return reporter.warn( 'There was an error processing static query' );
234 | }
235 |
236 | cacheQueryResults( hash, json, 'static', );
237 |
238 | }
239 |
240 | }
241 | }
242 |
243 | reporter.success( 'Finished getting files for query extraction' );
244 |
245 | }
246 |
247 | /**
248 | * Cache fragments.
249 | *
250 | * @param {string} fragmentsDir
251 | * @param {Object} cache
252 | * @return {bool} if succesfully cached.
253 | */
254 | async function cacheFragments( fragmentsDir, cache ) {
255 |
256 | const index = path.join( fragmentsDir, 'index.js' );
257 |
258 | if( !! fs.readFileSync( index ) ) {
259 |
260 | delete require.cache[ require.resolve( index ) ];
261 |
262 | fragments = require( index );
263 |
264 | await cache.set( 'groq-fragments', fragments );
265 |
266 | reporter.info( 'Cached fragments' );
267 |
268 | return true;
269 |
270 | }
271 |
272 | return false;
273 |
274 | }
275 |
276 | /**
277 | * Run page query and update the related page via createPage.
278 | *
279 | * @param {Object} $0 Gatsby Node Helpers.
280 | */
281 | async function pageQueryToContext( { actions, cache, file, getNodes, nodes, page, } ) {
282 |
283 | const { createPage, deletePage, setPageData } = actions;
284 |
285 | // Get query content.
286 | const content = fs.readFileSync( file, 'utf8' );
287 | let { unprocessed: query } = JSON.parse( content );
288 |
289 | // Replace any variables within query with context values.
290 | if( page.context ){
291 |
292 | for( let [ key, value ] of Object.entries( page.context ) ) {
293 |
294 | const search = `\\$${key}`;
295 | const pattern = new RegExp( search, 'g' );
296 | query = query.replace( pattern, `"${value}"` );
297 |
298 | }
299 | }
300 |
301 | // Do the thing.
302 | try {
303 |
304 | const allNodes = nodes || getNodes();
305 | const fragments = await cache.get( 'groq-fragments' );
306 | const { result } = await runQuery( query, allNodes, { file, fragments } );
307 |
308 | page.context.data = result;
309 |
310 | deletePage( page );
311 | createPage( {
312 | ...page,
313 | } );
314 | }
315 | catch( err ) {
316 | console.error( page.component );
317 | reporter.error( `${err}` );
318 | reporter.error( query );
319 | }
320 |
321 |
322 | }
323 |
324 | /**
325 | * Custom webpack.
326 | */
327 | exports.onCreateWebpackConfig = async ( { actions, cache, plugins, store } ) => {
328 |
329 |
330 | // Make sure we have have access to GROQ_DIR for useGroqQuery().
331 | actions.setWebpackConfig( {
332 | plugins: [
333 | plugins.define( {
334 | 'process.env.GROQ_DIR': JSON.stringify( GROQ_DIR ),
335 | } )
336 | ]
337 | } );
338 |
339 | }
340 |
341 | /**
342 | * Extracts page query from file and returns its hash and unprocessed string.
343 | *
344 | * @param {string} file
345 | * @param {map} nodes
346 | * @param {Object} cache
347 | * @return {Object} fileHash and query
348 | */
349 | async function processFilePageQuery( file, nodes, cache ) {
350 |
351 | const contents = fs.readFileSync( file, 'utf8' );
352 | const match = /export const groqQuery = /.exec( contents );
353 | if( ! match ) {
354 | return;
355 | }
356 |
357 | const ast = parse( file, contents );
358 | if( ! ast ) {
359 | return null;
360 | }
361 |
362 | let pageQuery = null;
363 | let queryStart = null;
364 | let queryEnd = null;
365 |
366 | traverse( ast, {
367 | ExportNamedDeclaration: function( path ) {
368 |
369 | const declarator = path.node.declaration.declarations[0];
370 |
371 | if( declarator.id.name === 'groqQuery' ) {
372 |
373 | queryStart = declarator.init.start;
374 | queryEnd = declarator.init.end;
375 | pageQuery = contents.substring( queryStart, queryEnd );
376 |
377 | }
378 |
379 | }
380 | } );
381 |
382 | if( ! pageQuery ) {
383 | return;
384 | }
385 |
386 | const hash = hashQuery( file );
387 |
388 | return {
389 | fileHash: hash,
390 | query: JSON.stringify( { unprocessed: pageQuery } ),
391 | }
392 |
393 | }
394 |
395 | /**
396 | * Extracts static query from file and returns its hash and result.
397 | *
398 | * @param {string} file
399 | * @param {map} nodes
400 | * @param {Object} cache
401 | * @return {array}
402 | */
403 | async function processFileStaticQueries( file, nodes, cache ) {
404 |
405 | const contents = fs.readFileSync( file, 'utf8' );
406 | const match = /useGroqQuery/.exec( contents );
407 | if( ! match ) {
408 | return;
409 | }
410 |
411 | const ast = parse( file, contents );
412 | if( ! ast ) {
413 | return null;
414 | }
415 |
416 | const staticQueries = [];
417 | let queryStart = null;
418 | let queryEnd = null;
419 |
420 | traverse( ast, {
421 | CallExpression: function( path ) {
422 |
423 | if( !! path.node.callee && path.node.callee.name === 'useGroqQuery' ) {
424 |
425 | queryStart = path.node.arguments[0].start + 1;
426 | queryEnd = path.node.arguments[0].end - 1;
427 |
428 | staticQueries.push( contents.substring( queryStart, queryEnd ) );
429 |
430 | }
431 |
432 | }
433 | } );
434 |
435 | if( ! staticQueries.length ) {
436 | return null;
437 | }
438 |
439 | const fragments = await cache.get( 'groq-fragments' );
440 | const results = [];
441 |
442 | for( let staticQuery of staticQueries ) {
443 |
444 | const { result, finalQuery, queryToHash } = await runQuery( staticQuery, nodes, { file, fragments } );
445 | if( result instanceof Error ) {
446 | results.push( result );
447 | }
448 |
449 | const hash = hashQuery( queryToHash );
450 | const json = JSON.stringify( result );
451 |
452 | results.push( { hash, json } );
453 |
454 | }
455 |
456 |
457 | return results;
458 |
459 | }
460 |
461 | /**
462 | * Cache result from query extraction.
463 | * For page queries this caches the query itself.
464 | * For static queries this caches the results of the query.
465 | *
466 | * @param {number} hash Hash to the json file.
467 | * @param {Object|string} data Data we are caching.
468 | * @param {string} type Page or static query. Optional. Default 'page'
469 | */
470 | async function cacheQueryResults( hash, data, type = 'page' ) {
471 |
472 | reporter.info( `Caching ${type} query: ${hash}` );
473 |
474 | const json = typeof data !== 'string' ? JSON.stringify( data ) : data;
475 |
476 | if( process.env.NODE_ENV === 'development' ) {
477 |
478 | // Probably a more sophisticated Gatsby way of doing this.
479 | fs.writeFileSync( `${GROQ_DIR}/${hash}.json`, json, err => {
480 | if( err ) {
481 | throw new Error( err );
482 | }
483 | } );
484 |
485 | }
486 | else {
487 |
488 | fs.writeFileSync( `${GROQ_DIR}/${hash}.json`, json, err => {
489 | if( err ) {
490 | throw new Error( err );
491 | }
492 | } );
493 |
494 | }
495 |
496 | }
497 |
498 | /**
499 | * Parse.
500 | *
501 | * @param {string} filePath
502 | * @param {string} content
503 | * @param {Object} options
504 | * @return {ast}
505 | */
506 | function parse( filePath, content, options = {} ) {
507 |
508 | const { plugins: additionalPlugins, ...additionalOptions } = options;
509 | let plugins = [ 'jsx' ];
510 |
511 | if( !! additionalPlugins ) {
512 | plugins = [ ...plugins, ...additionalPlugins ];
513 | }
514 |
515 | try {
516 |
517 | const ast = parser.parse( content, {
518 | errorRecovery: true,
519 | plugins,
520 | sourceType: 'module',
521 | ...additionalOptions,
522 | } );
523 |
524 | return ast;
525 |
526 | }
527 | catch( err ) {
528 | console.warn( `Error parsing file: ${filePath}` );
529 | console.log( 'Check below for more info' );
530 | return null;
531 | }
532 |
533 | }
534 |
535 | /**
536 | * Generate a hash based on the query.
537 | *
538 | * @param {string} query
539 | * @return {number}
540 | */
541 | function hashQuery( query ) {
542 | return murmurhash( query );
543 | }
544 |
--------------------------------------------------------------------------------