Skip to content

Commit da0f05a

Browse files
committed
feat(SLB-202): publish @amazeelabs/gatsby-plugin-operations
1 parent 9eb4eec commit da0f05a

File tree

14 files changed

+606
-167
lines changed

14 files changed

+606
-167
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.turbo
2+
build
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!build
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Gatsby operations
2+
3+
This Gatsby plugin allows to use persisted query ids provided by
4+
[@amazeelabs/codegen-operation-ids] within templates and in the `createPages`
5+
hook.
6+
7+
## Installation
8+
9+
Install the package and configure the plugin within Gatsby. The only argument is
10+
the path to the file of generated operation ids:
11+
12+
```js
13+
export const plugins = {
14+
{
15+
resolve: '@amazeelabs/gatsby-plugin-operations',
16+
options: {
17+
operations: './node_modules/@custom/schema/build/operations.json',
18+
},
19+
}
20+
}
21+
```
22+
23+
To get proper type checking, you have to augment Gatsby type definitions, by
24+
placing this anywhere in the `src` directory:
25+
26+
```typescript
27+
import {
28+
AnyOperationId,
29+
OperationResult,
30+
OperationVariables,
31+
} from '@custom/schema';
32+
33+
declare module '@amazeelabs/gatsby-plugin-operations' {
34+
export const graphql: <OperationId extends AnyOperationId>(
35+
id: OperationId,
36+
) => OperationResult<OperationId>;
37+
38+
function useStaticQuery<Input extends any>(id: Input): Input;
39+
40+
function graphqlQuery<OperationId extends AnyOperationId>(
41+
id: OperationId,
42+
vars?: OperationVariables<OperationId>,
43+
): Promise<{
44+
data: OperationResult<OperationId>;
45+
errors?: Array<any>;
46+
}>;
47+
}
48+
```
49+
50+
This relies on the build output of [@amazeelabs/codegen-operation-ids] being
51+
exported by `@custom/schema`.
52+
53+
## Usage
54+
55+
### In templates
56+
57+
For template queries, simply use `graphqlOperation` to define the query export.
58+
The query variable can be used directly to infer the template components
59+
properties.
60+
61+
```typescript
62+
import { graphql } from '@amazeelabs/gatsby-plugin-operations';
63+
import { ViewPageQuery } from '@custom/schema';
64+
65+
export const query = graphql(ViewPageQuery);
66+
export default function Page({
67+
data,
68+
pageContext,
69+
}: PageProps<typeof query>) {
70+
return <div>...</div>
71+
}
72+
```
73+
74+
### In static queries
75+
76+
To run a static query, one can use `graphql` in combination with
77+
`useStaticQuery`. This will yield a fully typed result of the requested query.
78+
79+
```typescript
80+
import { useStaticQuery, graphql } from '@amazeelabs/gatsby-plugin-operations';
81+
import { ListProductsQuery } from '@custom/schema';
82+
83+
const myResult = useStaticQuery(graphql(ListProductsQuery));
84+
```
85+
86+
### In `gatsby-node.mjs`
87+
88+
The `@amazeelabs/gatsby-plugin-operations` package provides a `graphqlQuery`
89+
function that works within the `createPages` hook.
90+
91+
```typescript
92+
import { graphqlQuery } from '@amazeelabs/gatsby-plugin-operations';
93+
import { ListPagesQuery } from '@custom/schema';
94+
95+
export const createPages({actions}) {
96+
const result = await graphqlQuery(ListPagesQuery);
97+
result.page.forEach((page) => {
98+
actions.createPage({});
99+
})
100+
}
101+
```
102+
103+
[@amazeelabs/codegen-operation-ids]:
104+
https://www.npmjs.com/package/@amazeelabs/codegen-operation-ids
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('./build/gatsby-node.js');
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "@amazeelabs/gatsby-plugin-operations",
3+
"version": "1.0.0",
4+
"description": "",
5+
"type": "commonjs",
6+
"main": "build/index.js",
7+
"types": "build/index.d.ts",
8+
"private": false,
9+
"publishConfig": {
10+
"access": "public"
11+
},
12+
"scripts": {
13+
"test": "vitest run",
14+
"prep": "tsc",
15+
"build": "pnpm prep"
16+
},
17+
"keywords": [],
18+
"author": "",
19+
"license": "ISC",
20+
"devDependencies": {
21+
"@babel/preset-react": "^7.23.3",
22+
"@babel/preset-typescript": "^7.23.3",
23+
"@babel/types": "^7.23.6",
24+
"@types/babel__core": "^7.20.5",
25+
"@types/node": "^18",
26+
"babel-plugin-tester": "^11.0.4",
27+
"typescript": "^5.3.3",
28+
"vitest": "^1.1.0",
29+
"gatsby": ">= 5"
30+
},
31+
"dependencies": {
32+
"@babel/core": "^7.23.6"
33+
}
34+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { transformSync } from '@babel/core';
2+
import type { GatsbyNode } from 'gatsby';
3+
4+
import { initialize } from './graphql.js';
5+
import babelPlugin from './plugin.js';
6+
7+
export const onCreateBabelConfig: GatsbyNode['onCreateBabelConfig'] = (
8+
{ actions },
9+
options,
10+
) => {
11+
// Inject the babel plugin into webpack.
12+
actions.setBabelPlugin({ name: require.resolve('./plugin'), options });
13+
};
14+
15+
/**
16+
* Replace query id's with gatsby graphql`` tags before queries are collected.
17+
*/
18+
export const preprocessSource: GatsbyNode['preprocessSource'] = (
19+
{ contents },
20+
options,
21+
) => {
22+
const result = transformSync(contents, {
23+
plugins: [[babelPlugin, options]],
24+
presets: [
25+
'@babel/preset-react',
26+
['@babel/preset-typescript', { isTSX: true, allExtensions: true }],
27+
],
28+
});
29+
return result?.code ? result.code : contents;
30+
};
31+
32+
/**
33+
* Make persisted queries and the graphql function globally
34+
* available.
35+
*/
36+
export const createPages: GatsbyNode['createPages'] = (
37+
{ graphql },
38+
options,
39+
) => {
40+
initialize(graphql, options.operations as string);
41+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { readFileSync } from 'fs';
2+
import type { CreatePagesArgs } from 'gatsby';
3+
4+
export let _graphql: CreatePagesArgs['graphql'] | undefined = undefined;
5+
let _operations: Record<string, string> | undefined = undefined;
6+
7+
/**
8+
* Initialize the library. Happens in `./gatsby-node.ts`
9+
*/
10+
export function initialize(
11+
graphql: CreatePagesArgs['graphql'],
12+
operations: string,
13+
) {
14+
_graphql = graphql;
15+
_operations = JSON.parse(readFileSync(operations).toString());
16+
}
17+
18+
/**
19+
* Execute a graphql query against gatsby.
20+
*/
21+
export function graphqlQuery(id: string, vars?: any): any {
22+
if (!_graphql || !_operations) {
23+
throw new Error(
24+
'Plugin "@amazeelabs/gatsby-plugin-operations" not available. Make sure its configured in "gatsby-config.mjs" and that "graphqlQuery" is used within "createPages" only.',
25+
);
26+
}
27+
const operation = _operations?.[id];
28+
return _graphql(operation, vars);
29+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { graphqlQuery } from './graphql';
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { pluginTester } from 'babel-plugin-tester';
2+
3+
import plugin from './plugin.js';
4+
5+
pluginTester({
6+
plugin,
7+
pluginOptions: {
8+
operations: `./test/operations.json`,
9+
},
10+
babelOptions: {
11+
presets: [
12+
'@babel/preset-react',
13+
['@babel/preset-typescript', { isTSX: true, allExtensions: true }],
14+
],
15+
},
16+
tests: [
17+
{
18+
title: 'Page query',
19+
code: `
20+
import { MyOperation } from '@custom/schema';
21+
import { graphql } from '@amazeelabs/gatsby-plugin-operations';
22+
export const query = graphql(MyOperation);
23+
`,
24+
output: `
25+
import { MyOperation } from '@custom/schema';
26+
import { graphql } from 'gatsby';
27+
export const query = graphql\`
28+
{
29+
field
30+
}
31+
\`;`,
32+
},
33+
{
34+
title: 'Static query',
35+
code: `
36+
import { MyOperation } from '@custom/schema';
37+
import { graphql, useStaticQuery } from '@amazeelabs/gatsby-plugin-operations';
38+
function useData() {
39+
return useStaticQuery(graphql(MyOperation));
40+
}`,
41+
output: `
42+
import { MyOperation } from '@custom/schema';
43+
import { graphql, useStaticQuery } from 'gatsby';
44+
function useData() {
45+
return useStaticQuery(
46+
graphql\`
47+
{
48+
field
49+
}
50+
\`,
51+
);
52+
}`,
53+
},
54+
{
55+
title: 'Typescript',
56+
code: `
57+
import { MyOperation } from '@custom/schema';
58+
import { graphql, useStaticQuery } from '@amazeelabs/gatsby-plugin-operations';
59+
function useData() {
60+
return useStaticQuery(graphql(MyOperation));
61+
}
62+
63+
export function Component(props: { message: string }) {
64+
return <div>{props.message}</div>;
65+
}
66+
`,
67+
output: `
68+
import { MyOperation } from '@custom/schema';
69+
import { graphql, useStaticQuery } from 'gatsby';
70+
function useData() {
71+
return useStaticQuery(
72+
graphql\`
73+
{
74+
field
75+
}
76+
\`,
77+
);
78+
}
79+
export function Component(props) {
80+
return /*#__PURE__*/ React.createElement('div', null, props.message);
81+
}`,
82+
},
83+
],
84+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { PluginObj, PluginPass, types } from '@babel/core';
2+
import { readFileSync } from 'fs';
3+
4+
function loadOperations(path: string) {
5+
const loadedOperations: Record<string, string> = {};
6+
const loaded = JSON.parse(readFileSync(path).toString());
7+
Object.keys(loaded).forEach((key) => {
8+
loadedOperations[key.split(':')[0]] = loaded[key];
9+
});
10+
return loadedOperations;
11+
}
12+
13+
export default () =>
14+
({
15+
visitor: {
16+
ImportDeclaration(path) {
17+
if (path.node.source.value === '@amazeelabs/gatsby-plugin-operations') {
18+
path.replaceWith(
19+
types.importDeclaration(
20+
path.node.specifiers,
21+
types.stringLiteral('gatsby'),
22+
),
23+
);
24+
path.skip();
25+
}
26+
},
27+
CallExpression(path, { opts }) {
28+
const operations = loadOperations(opts.operations);
29+
if (path.node.callee.type === 'Identifier') {
30+
if (path.node.callee.name === 'graphql') {
31+
if (
32+
!(
33+
path.node.arguments.length === 1 &&
34+
path.node.arguments[0].type === 'Identifier'
35+
)
36+
) {
37+
return;
38+
}
39+
const operation = path.node.arguments[0].name;
40+
if (!operation || !operations[operation]) {
41+
return;
42+
}
43+
path.replaceWith(
44+
types.taggedTemplateExpression(
45+
types.identifier('graphql'),
46+
types.templateLiteral(
47+
[types.templateElement({ raw: operations[operation] })],
48+
[],
49+
),
50+
),
51+
);
52+
path.skip();
53+
return;
54+
}
55+
}
56+
},
57+
},
58+
}) satisfies PluginObj<
59+
PluginPass & {
60+
opts: {
61+
operations: string;
62+
};
63+
}
64+
>;

0 commit comments

Comments
 (0)