Skip to content

Commit

Permalink
GraphQL Mesh will now be in read-only mode by default, so only a sing…
Browse files Browse the repository at this point in the history
…le instance is created globally
  • Loading branch information
paales committed Jan 16, 2025
1 parent 1132991 commit 4075474
Show file tree
Hide file tree
Showing 12 changed files with 133 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .changeset/strange-turkeys-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphcommerce/graphql-mesh': patch
---

GraphQL Mesh will now be in read-only mode by default, so only a single instance is created globally. This means it doesn't get recreated on each page compilation and fast refresh. Creating the instance is an expensive operation and can take multiple seconds and during development (and this can happen multiple times during a single change). Now only a single instance is created during development. To make sure changes are picked up during development set the config value `graphqlMeshEditMode: true` in your graphcommerce.config.js or set the env variable `GC_GRAPHQL_MESH_EDIT_MODE=1`. This _will_ make the frontend considerably slower.
5 changes: 5 additions & 0 deletions docs/framework/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ The Google Tagmanager ID to be used on the site.

This value is required even if you are configuring different values for each locale.

#### graphqlMeshEditMode: boolean = `false`

The GraphQL Mesh will be loaded once and any modifications to resolvers will be ignored. When developing
new resolvers this should be set to true.

#### hygraphManagementApi: string

Hygraph Management API. **Only used for migrations.**
Expand Down
30 changes: 29 additions & 1 deletion docs/framework/mesh.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ To make modifications to the Mesh configuration, you can:
### Modify the meshrc.yaml:

You can always modify the base configuration of the Mesh by modifying the
`meshrc.yaml` file.
`meshrc.yaml` file. After making always run `yarn codegen` (this can be in a
separate terminal and nextjs will reload it).

### Write a plugin:

Expand Down Expand Up @@ -61,6 +62,33 @@ export const meshConfig: FunctionPlugin<typeof meshConfigBase> = (
},
},
],
additionalResolvers: [
...(baseConfig.additionalResolvers ?? []),
'lib/resolvers/my-feature.ts',
],
})
}
```

### Creating additional schema's

During development it might come in handy to write schema extensions even before
any backend work has been done. `AnyFile.graphqls` in the graphql directory will
automatically be picked up and merged with the rest of the schema.

### Creating additional resolvers

In the plugin add additionalResolvers and point to your ts file where the
resolver is.

```tsx
// This MUST be a type import, else there will be a circular dependency.
import type { Resolvers } from '@graphcommerce/graphql-mesh'

const resolvers: Resolvers = {}
```

To make sure changes are picked up during development set the config value
`graphqlMeshEditMode: true` in your graphcommerce.config.js or set the env
variable `GC_GRAPHQL_MESH_EDIT_MODE=1`. This _will_ make the frontend
considerably slower.
13 changes: 10 additions & 3 deletions packages/cli/dist/bin/mesh.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import dotenv from 'dotenv';
import 'tsx/cjs';
import 'tsx/esm';
import yaml from 'yaml';
import { cosmiconfig, defaultLoaders } from 'cosmiconfig';
import path from 'path';
import { cosmiconfig, defaultLoaders } from 'cosmiconfig';

function customLoader(ext, importFn = defaultImportFn, initialLoggerPrefix = "\u{1F578}\uFE0F Mesh") {
const logger = new DefaultLogger(initialLoggerPrefix).child("config");
Expand Down Expand Up @@ -40,7 +40,7 @@ function customLoader(ext, importFn = defaultImportFn, initialLoggerPrefix = "\u
return loader;
}
async function findConfig(options) {
const { configName = "mesh", dir: configDir = "", initialLoggerPrefix } = options || {};
const { configName = "mesh", dir: configDir = "", initialLoggerPrefix } = options;
const dir = path.isAbsolute(configDir) ? configDir : path.join(process.cwd(), configDir);
const explorer = cosmiconfig(configName, {
searchPlaces: [
Expand Down Expand Up @@ -169,7 +169,14 @@ const main = async () => {
await promises.writeFile(tmpMeshLocation, yamlString);
await promises.writeFile(
`${meshDir}/.mesh.ts`,
`export * from '${relativePath.split(path$1.sep).join("/")}.mesh'`,
`export type * from '${relativePath.split(path$1.sep).join("/")}.mesh'
export {
getBuiltMesh as getBuiltMeshBase,
execute,
subscribe,
createBuiltMeshHTTPHandler as createBuiltMeshHTTPHandlerBase,
rawServeConfig,
} from '${relativePath.split(path$1.sep).join("/")}.mesh'`,
{ encoding: "utf8" }
);
await graphqlMesh({ ...cliParams, configName: tmpMesh });
Expand Down
9 changes: 8 additions & 1 deletion packages/cli/src/bin/mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,14 @@ const main = async () => {
// Reexport the mesh to is can be used by packages
await fs.writeFile(
`${meshDir}/.mesh.ts`,
`export * from '${relativePath.split(path.sep).join('/')}.mesh'`,
`export type * from '${relativePath.split(path.sep).join('/')}.mesh'
export {
getBuiltMesh as getBuiltMeshBase,
execute,
subscribe,
createBuiltMeshHTTPHandler as createBuiltMeshHTTPHandlerBase,
rawServeConfig,
} from '${relativePath.split(path.sep).join('/')}.mesh'`,
{ encoding: 'utf8' },
)

Expand Down
7 changes: 7 additions & 0 deletions packages/graphql-mesh/Config.graphqls
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
extend input GraphCommerceConfig {
"""
The GraphQL Mesh will be loaded once and any modifications to resolvers will be ignored. When developing
new resolvers this should be set to true.
"""
graphqlMeshEditMode: Boolean = false
}
10 changes: 5 additions & 5 deletions packages/graphql-mesh/api/createEnvelop.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import { createBuiltMeshHTTPHandler } from '../.mesh'

const handler = createBuiltMeshHTTPHandler()
import { createBuiltMeshHTTPHandler } from './globalThisMesh'

// eslint-disable-next-line @typescript-eslint/require-await
export const createServer = async (endpoint: string) => {
if (endpoint !== '/api/graphql')
throw Error('Moving the GraphQL Endpoint is not supported at the moment')
return (req: NextApiRequest, res: NextApiResponse) => {

const handler = createBuiltMeshHTTPHandler()
return async (req: NextApiRequest, res: NextApiResponse) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*')
const requestedHeaders = req.headers['access-control-request-headers']
if (requestedHeaders) {
Expand All @@ -20,6 +20,6 @@ export const createServer = async (endpoint: string) => {
return
}

handler(req, res)
await handler(req, res)
}
}
55 changes: 55 additions & 0 deletions packages/graphql-mesh/api/globalThisMesh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { MeshInstance } from '@graphql-mesh/runtime'
import type {
ServerAdapter,
ServerAdapterBaseObject,
ServerAdapterRequestHandler,
} from '@whatwg-node/server'
import { createBuiltMeshHTTPHandlerBase, getBuiltMeshBase } from '../.mesh'

type MeshHTTPHandler<TServerContext = Record<string, unknown>> = ServerAdapter<
TServerContext,
ServerAdapterBaseObject<TServerContext, ServerAdapterRequestHandler<TServerContext>>
>

declare global {
// eslint-disable-next-line vars-on-top, no-var
var buildMesh: Promise<MeshInstance> | undefined

// eslint-disable-next-line vars-on-top, no-var
var builtMeshHandler: MeshHTTPHandler | undefined
}

const shouldGlobalThisMeshBeCreated =
process.env.NODE_ENV === 'development' && import.meta.graphCommerce.graphqlMeshEditMode !== true

/**
* We are creating a global instance of the mesh so it doesn't get recreated on every change.
* Creating the instance is a very long operation and with sufficiently complex schema's it can take
* multiple seconds. During development it can happen multiple times during a single change.
*
* During development this creates a big advantage as we do not recreate the mesh on every reload.
* This makes development a lot faster.
*
* The disadvantage of this is that the mesh and any resolvers custom resolver will not be refreshed
* whenever code changes are made, to enable this set the config value `graphqlMeshEditMode: true`
* in your graphcommerce.config.js or set the env variable `GC_GRAPHQL_MESH_EDIT_MODE=1`.
*/
export function getBuiltMesh() {
if (shouldGlobalThisMeshBeCreated) {
globalThis.buildMesh ??= getBuiltMeshBase()
return globalThis.buildMesh
}
return getBuiltMeshBase()
}

/**
* Same as globalThisGetBuiltMesh but for the mesh handler. As the handler uses additional logic so
* we can't re-use globalThisGetBuiltMesh.
*/
export function createBuiltMeshHTTPHandler(): MeshHTTPHandler {
if (shouldGlobalThisMeshBeCreated) {
globalThis.builtMeshHandler ??= createBuiltMeshHTTPHandlerBase() as MeshHTTPHandler
return globalThis.builtMeshHandler
}
return createBuiltMeshHTTPHandlerBase() as MeshHTTPHandler
}
1 change: 1 addition & 0 deletions packages/graphql-mesh/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './api/createEnvelop'
export * from './api/apolloLink'
export * from './.mesh'
export * from './api/globalThisMesh'
export * from './utils/traverseSelectionSet'
2 changes: 1 addition & 1 deletion packages/graphql-mesh/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"exclude": ["**/node_modules", "**/.*/"],
"include": ["**/*.ts", "**/*.tsx"],
"extends": "@graphcommerce/typescript-config-pwa/node.json"
"extends": "@graphcommerce/typescript-config-pwa/nextjs.json"
}
1 change: 1 addition & 0 deletions packagesDev/next-config/dist/generated/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ function GraphCommerceConfigSchema() {
googlePlaystore: GraphCommerceGooglePlaystoreConfigSchema().nullish(),
googleRecaptchaKey: _zod.z.string().nullish(),
googleTagmanagerId: _zod.z.string().nullish(),
graphqlMeshEditMode: _zod.z.boolean().default(false).nullish(),
hygraphEndpoint: _zod.z.string().min(1),
hygraphManagementApi: _zod.z.string().nullish(),
hygraphProjectId: _zod.z.string().nullish(),
Expand Down
6 changes: 6 additions & 0 deletions packagesDev/next-config/src/generated/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,11 @@ export type GraphCommerceConfig = {
* This value is required even if you are configuring different values for each locale.
*/
googleTagmanagerId?: InputMaybe<Scalars['String']['input']>
/**
* The GraphQL Mesh will be loaded once and any modifications to resolvers will be ignored. When
* developing new resolvers this should be set to true.
*/
graphqlMeshEditMode?: InputMaybe<Scalars['Boolean']['input']>
/**
* The HyGraph endpoint.> Read-only endpoint that allows low latency and high read-throughput content delivery.
*
Expand Down Expand Up @@ -610,6 +615,7 @@ export function GraphCommerceConfigSchema(): z.ZodObject<Properties<GraphCommerc
googlePlaystore: GraphCommerceGooglePlaystoreConfigSchema().nullish(),
googleRecaptchaKey: z.string().nullish(),
googleTagmanagerId: z.string().nullish(),
graphqlMeshEditMode: z.boolean().default(false).nullish(),
hygraphEndpoint: z.string().min(1),
hygraphManagementApi: z.string().nullish(),
hygraphProjectId: z.string().nullish(),
Expand Down

0 comments on commit 4075474

Please sign in to comment.