Skip to content

Commit

Permalink
feat(SLB-202): publish @amazeelabs/gatsby-plugin-operations
Browse files Browse the repository at this point in the history
  • Loading branch information
pmelab committed Jan 18, 2024
1 parent 9eb4eec commit 7efbf9d
Show file tree
Hide file tree
Showing 13 changed files with 387 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/npm/@amazeelabs/gatsby-plugin-operations/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.turbo
build
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!build
104 changes: 104 additions & 0 deletions packages/npm/@amazeelabs/gatsby-plugin-operations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Gatsby operations

This Gatsby plugin allows to use persisted query ids provided by
[@amazeelabs/codegen-operation-ids] within templates and in the `createPages`
hook.

## Installation

Install the package and configure the plugin within Gatsby. The only argument is
the path to the file of generated operation ids:

```js
export const plugins = {
{
resolve: '@amazeelabs/gatsby-plugin-operations',
options: {
operations: './node_modules/@custom/schema/build/operations.json',
},
}
}
```

To get proper type checking, you have to augment Gatsby type definitions, by
placing this anywhere in the `src` directory:

```typescript
import {
AnyOperationId,
OperationResult,
OperationVariables,
} from '@custom/schema';

declare module '@amazeelabs/gatsby-plugin-operations' {
export const graphql: <OperationId extends AnyOperationId>(
id: OperationId,
) => OperationResult<OperationId>;

function useStaticQuery<Input extends any>(id: Input): Input;

function graphqlQuery<OperationId extends AnyOperationId>(
id: OperationId,
vars?: OperationVariables<OperationId>,
): Promise<{
data: OperationResult<OperationId>;
errors?: Array<any>;
}>;
}
```

This relies on the build output of [@amazeelabs/codegen-operation-ids] being
exported by `@custom/schema`.

## Usage

### In templates

For template queries, simply use `graphqlOperation` to define the query export.
The query variable can be used directly to infer the template components
properties.

```typescript
import { graphql } from '@amazeelabs/gatsby-plugin-operations';
import { ViewPageQuery } from '@custom/schema';

export const query = graphql(ViewPageQuery);
export default function Page({
data,
pageContext,
}: PageProps<typeof query>) {
return <div>...</div>
}
```

### In static queries

To run a static query, one can use `graphql` in combination with
`useStaticQuery`. This will yield a fully typed result of the requested query.

```typescript
import { useStaticQuery, graphql } from '@amazeelabs/gatsby-plugin-operations';
import { ListProductsQuery } from '@custom/schema';

const myResult = useStaticQuery(graphql(ListProductsQuery));
```

### In `gatsby-node.mjs`

The `@amazeelabs/gatsby-plugin-operations` package provides a `graphqlQuery`
function that works within the `createPages` hook.

```typescript
import { graphqlQuery } from '@amazeelabs/gatsby-plugin-operations';
import { ListPagesQuery } from '@custom/schema';

export const createPages({actions}) {
const result = await graphqlQuery(ListPagesQuery);
result.page.forEach((page) => {
actions.createPage({});
})
}
```

[@amazeelabs/codegen-operation-ids]:
https://www.npmjs.com/package/@amazeelabs/codegen-operation-ids
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./build/gatsby-node.js');
34 changes: 34 additions & 0 deletions packages/npm/@amazeelabs/gatsby-plugin-operations/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@amazeelabs/gatsby-plugin-operations",
"version": "1.0.0",
"description": "",
"type": "commonjs",
"main": "build/index.js",
"types": "build/index.d.ts",
"private": false,
"publishConfig": {
"access": "public"
},
"scripts": {
"test": "vitest run",
"prep": "tsc",
"build": "pnpm prep"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@babel/types": "^7.23.6",
"@types/babel__core": "^7.20.5",
"@types/node": "^18",
"babel-plugin-tester": "^11.0.4",
"typescript": "^5.3.3",
"vitest": "^1.1.0",
"gatsby": ">= 5"
},
"dependencies": {
"@babel/core": "^7.23.6"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { transformSync } from '@babel/core';
import type { GatsbyNode } from 'gatsby';

import { initialize } from './graphql.js';
import babelPlugin from './plugin.js';

export const onCreateBabelConfig: GatsbyNode['onCreateBabelConfig'] = (
{ actions },
options,
) => {
// Inject the babel plugin into webpack.
actions.setBabelPlugin({ name: require.resolve('./plugin'), options });
};

/**
* Replace query id's with gatsby graphql`` tags before queries are collected.
*/
export const preprocessSource: GatsbyNode['preprocessSource'] = (
{ contents },
options,
) => {
const result = transformSync(contents, {
plugins: [[babelPlugin, options]],
presets: [
'@babel/preset-react',
['@babel/preset-typescript', { isTSX: true, allExtensions: true }],
],
});
return result?.code ? result.code : contents;
};

/**
* Make persisted queries and the graphql function globally
* available.
*/
export const createPages: GatsbyNode['createPages'] = (
{ graphql },
options,
) => {
initialize(graphql, options.operations as string);
};
29 changes: 29 additions & 0 deletions packages/npm/@amazeelabs/gatsby-plugin-operations/src/graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { readFileSync } from 'fs';
import type { CreatePagesArgs } from 'gatsby';

export let _graphql: CreatePagesArgs['graphql'] | undefined = undefined;
let _operations: Record<string, string> | undefined = undefined;

/**
* Initialize the library. Happens in `./gatsby-node.ts`
*/
export function initialize(
graphql: CreatePagesArgs['graphql'],
operations: string,
) {
_graphql = graphql;
_operations = JSON.parse(readFileSync(operations).toString());
}

/**
* Execute a graphql query against gatsby.
*/
export function graphqlQuery(id: string, vars?: any): any {
if (!_graphql || !_operations) {
throw new Error(
'Plugin "@amazeelabs/gatsby-plugin-operations" not available. Make sure its configured in "gatsby-config.mjs" and that "graphqlQuery" is used within "createPages" only.',
);
}
const operation = _operations?.[id];
return _graphql(operation, vars);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { graphqlQuery } from './graphql';
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { pluginTester } from 'babel-plugin-tester';

import plugin from './plugin.js';

pluginTester({
plugin,
pluginOptions: {
operations: `./test/operations.json`,
},
babelOptions: {
presets: [
'@babel/preset-react',
['@babel/preset-typescript', { isTSX: true, allExtensions: true }],
],
},
tests: [
{
title: 'Page query',
code: `
import { MyOperation } from '@custom/schema';
import { graphql } from '@amazeelabs/gatsby-plugin-operations';
export const query = graphql(MyOperation);
`,
output: `
import { MyOperation } from '@custom/schema';
import { graphql } from 'gatsby';
export const query = graphql\`
{
field
}
\`;`,
},
{
title: 'Static query',
code: `
import { MyOperation } from '@custom/schema';
import { graphql, useStaticQuery } from '@amazeelabs/gatsby-plugin-operations';
function useData() {
return useStaticQuery(graphql(MyOperation));
}`,
output: `
import { MyOperation } from '@custom/schema';
import { graphql, useStaticQuery } from 'gatsby';
function useData() {
return useStaticQuery(
graphql\`
{
field
}
\`,
);
}`,
},
{
title: 'Typescript',
code: `
import { MyOperation } from '@custom/schema';
import { graphql, useStaticQuery } from '@amazeelabs/gatsby-plugin-operations';
function useData() {
return useStaticQuery(graphql(MyOperation));
}
export function Component(props: { message: string }) {
return <div>{props.message}</div>;
}
`,
output: `
import { MyOperation } from '@custom/schema';
import { graphql, useStaticQuery } from 'gatsby';
function useData() {
return useStaticQuery(
graphql\`
{
field
}
\`,
);
}
export function Component(props) {
return /*#__PURE__*/ React.createElement('div', null, props.message);
}`,
},
],
});
64 changes: 64 additions & 0 deletions packages/npm/@amazeelabs/gatsby-plugin-operations/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { PluginObj, PluginPass, types } from '@babel/core';
import { readFileSync } from 'fs';

function loadOperations(path: string) {
const loadedOperations: Record<string, string> = {};
const loaded = JSON.parse(readFileSync(path).toString());
Object.keys(loaded).forEach((key) => {
loadedOperations[key.split(':')[0]] = loaded[key];
});
return loadedOperations;
}

export default () =>
({
visitor: {
ImportDeclaration(path) {
if (path.node.source.value === '@amazeelabs/gatsby-plugin-operations') {
path.replaceWith(
types.importDeclaration(
path.node.specifiers,
types.stringLiteral('gatsby'),
),
);
path.skip();
}
},
CallExpression(path, { opts }) {
const operations = loadOperations(opts.operations);
if (path.node.callee.type === 'Identifier') {
if (path.node.callee.name === 'graphql') {
if (
!(
path.node.arguments.length === 1 &&
path.node.arguments[0].type === 'Identifier'
)
) {
return;
}
const operation = path.node.arguments[0].name;
if (!operation || !operations[operation]) {
return;
}
path.replaceWith(
types.taggedTemplateExpression(
types.identifier('graphql'),
types.templateLiteral(
[types.templateElement({ raw: operations[operation] })],
[],
),
),
);
path.skip();
return;
}
}
},
},
}) satisfies PluginObj<
PluginPass & {
opts: {
operations: string;
};
}
>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"MyOperation:1234": "{ field }"
}
15 changes: 15 additions & 0 deletions packages/npm/@amazeelabs/gatsby-plugin-operations/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ESNext",
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "CommonJS",
"resolveJsonModule": true,
"declaration": true,
"declarationDir": "build",
"lib": ["ESNext", "DOM"],
"outDir": "build"
},
"include": ["src"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
globals: true,
include: ['src/**/*.test.ts'],
},
});

0 comments on commit 7efbf9d

Please sign in to comment.