diff --git a/.changeset/@graphql-mesh_compose-cli-6862-dependencies.md b/.changeset/@graphql-mesh_compose-cli-6862-dependencies.md new file mode 100644 index 0000000000000..4d5dae8a5997e --- /dev/null +++ b/.changeset/@graphql-mesh_compose-cli-6862-dependencies.md @@ -0,0 +1,7 @@ +--- +"@graphql-mesh/compose-cli": patch +--- +dependencies updates: + - Added dependency [`@commander-js/extra-typings@^12.0.1` ↗︎](https://www.npmjs.com/package/@commander-js/extra-typings/v/12.0.1) (to `dependencies`) + - Added dependency [`commander@^12.0.0` ↗︎](https://www.npmjs.com/package/commander/v/12.0.0) (to `dependencies`) + - Removed dependency [`spinnies@^0.5.1` ↗︎](https://www.npmjs.com/package/spinnies/v/0.5.1) (from `dependencies`) diff --git a/.changeset/@graphql-mesh_serve-cli-6862-dependencies.md b/.changeset/@graphql-mesh_serve-cli-6862-dependencies.md new file mode 100644 index 0000000000000..014eec14dc0da --- /dev/null +++ b/.changeset/@graphql-mesh_serve-cli-6862-dependencies.md @@ -0,0 +1,7 @@ +--- +"@graphql-mesh/serve-cli": patch +--- +dependencies updates: + - Added dependency [`@commander-js/extra-typings@^12.0.1` ↗︎](https://www.npmjs.com/package/@commander-js/extra-typings/v/12.0.1) (to `dependencies`) + - Added dependency [`@graphql-tools/utils@^10.1.3` ↗︎](https://www.npmjs.com/package/@graphql-tools/utils/v/10.1.3) (to `dependencies`) + - Added dependency [`commander@^12.0.0` ↗︎](https://www.npmjs.com/package/commander/v/12.0.0) (to `dependencies`) diff --git a/.changeset/angry-cows-fetch.md b/.changeset/angry-cows-fetch.md new file mode 100644 index 0000000000000..d86927217c965 --- /dev/null +++ b/.changeset/angry-cows-fetch.md @@ -0,0 +1,5 @@ +--- +"@graphql-mesh/compose-cli": minor +--- + +Better CLI, supporting arguments and adding help in the shell diff --git a/.changeset/beige-bottles-lick.md b/.changeset/beige-bottles-lick.md new file mode 100644 index 0000000000000..c4ff97bc5af91 --- /dev/null +++ b/.changeset/beige-bottles-lick.md @@ -0,0 +1,33 @@ +--- +"@graphql-mesh/compose-cli": minor +--- + +Rename `target` option to `output` in order to be more clear that it's the output file. + +```diff +import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; +import { defineConfig } from '@graphql-mesh/compose-cli'; + +export const composeConfig = defineConfig({ +- target: 'fusiongraph.graphql', ++ output: 'fusiongraph.graphql', + subgraphs: [ + { + sourceHandler: () => ({ + name: 'helloworld', + schema$: new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + hello: { + type: GraphQLString, + resolve: () => 'world', + }, + }, + }), + }), + }), + }, + ], +}); +``` diff --git a/.changeset/long-eagles-draw.md b/.changeset/long-eagles-draw.md new file mode 100644 index 0000000000000..1252e43b9bec3 --- /dev/null +++ b/.changeset/long-eagles-draw.md @@ -0,0 +1,5 @@ +--- +"@graphql-mesh/compose-cli": minor +--- + +Rename `runComposeCLI` to just `run` and change the supported options diff --git a/.changeset/new-cycles-mate.md b/.changeset/new-cycles-mate.md new file mode 100644 index 0000000000000..842c0e76d9567 --- /dev/null +++ b/.changeset/new-cycles-mate.md @@ -0,0 +1,5 @@ +--- +"@graphql-mesh/serve-cli": minor +--- + +Better CLI, supporting arguments and adding help in the shell diff --git a/.changeset/stupid-baboons-play.md b/.changeset/stupid-baboons-play.md new file mode 100644 index 0000000000000..4997a2f1b0088 --- /dev/null +++ b/.changeset/stupid-baboons-play.md @@ -0,0 +1,5 @@ +--- +"@graphql-mesh/serve-cli": minor +--- + +Rename `runServeCLI` to just `run` and change the supported options diff --git a/e2e/auto-type-merging/auto-type-merging.test.ts b/e2e/auto-type-merging/auto-type-merging.test.ts index 6776bcb74e2ea..7ea6932a9e60c 100644 --- a/e2e/auto-type-merging/auto-type-merging.test.ts +++ b/e2e/auto-type-merging/auto-type-merging.test.ts @@ -35,10 +35,10 @@ it.concurrent.each([ `, }, ])('should execute $name', async ({ query }) => { - const { target } = await compose({ - target: 'graphql', + const { output } = await compose({ + output: 'graphql', services: [petstore, await service('vaccination')], }); - const { execute } = await serve({ fusiongraph: target }); + const { execute } = await serve({ fusiongraph: output }); await expect(execute({ query })).resolves.toMatchSnapshot(); }); diff --git a/e2e/auto-type-merging/mesh.config.ts b/e2e/auto-type-merging/mesh.config.ts index fd5bdc3f3ec27..bf7f53e23a25e 100644 --- a/e2e/auto-type-merging/mesh.config.ts +++ b/e2e/auto-type-merging/mesh.config.ts @@ -4,16 +4,14 @@ import { createFilterTransform, createNamingConventionTransform, createPrefixTransform, - defineConfig as defineComposeConfig, + defineConfig, loadGraphQLHTTPSubgraph, } from '@graphql-mesh/compose-cli'; -import { defineConfig as defineServeConfig } from '@graphql-mesh/serve-cli'; import { loadOpenAPISubgraph } from '@omnigraph/openapi'; const args = Args(process.argv); -export const composeConfig = defineComposeConfig({ - target: args.get('target'), +export const composeConfig = defineConfig({ subgraphs: [ { sourceHandler: loadOpenAPISubgraph('petstore', { @@ -45,20 +43,3 @@ export const composeConfig = defineComposeConfig({ }, ], }); - -export const serveConfig = defineServeConfig({ - port: args.getPort(), - fusiongraph: args.get('fusiongraph'), - graphiql: { - defaultQuery: /* GraphQL */ ` - query Test { - getPetById(petId: 1) { - __typename - id - name - vaccinated - } - } - `, - }, -}); diff --git a/e2e/batching-resolver/batching-resolver.test.ts b/e2e/batching-resolver/batching-resolver.test.ts index 047caee6aad61..3058bafd962fc 100644 --- a/e2e/batching-resolver/batching-resolver.test.ts +++ b/e2e/batching-resolver/batching-resolver.test.ts @@ -29,7 +29,7 @@ it.concurrent.each([ `, }, ])('should execute $name', async ({ query }) => { - const { target } = await compose({ target: 'graphql', services: [await service('api')] }); - const { execute } = await serve({ fusiongraph: target }); + const { output } = await compose({ output: 'graphql', services: [await service('api')] }); + const { execute } = await serve({ fusiongraph: output }); await expect(execute({ query })).resolves.toMatchSnapshot(); }); diff --git a/e2e/batching-resolver/mesh.config.ts b/e2e/batching-resolver/mesh.config.ts index 7f23e1435fe1a..1ed55e16b6adb 100644 --- a/e2e/batching-resolver/mesh.config.ts +++ b/e2e/batching-resolver/mesh.config.ts @@ -1,12 +1,10 @@ import { Args } from '@e2e/args'; -import { defineConfig as defineComposeConfig } from '@graphql-mesh/compose-cli'; -import { defineConfig as defineServeConfig } from '@graphql-mesh/serve-cli'; +import { defineConfig } from '@graphql-mesh/compose-cli'; import { loadOpenAPISubgraph } from '@omnigraph/openapi'; const args = Args(process.argv); -export const composeConfig = defineComposeConfig({ - target: args.get('target'), +export const composeConfig = defineConfig({ subgraphs: [ { sourceHandler: loadOpenAPISubgraph('API', { @@ -33,8 +31,3 @@ export const composeConfig = defineComposeConfig({ } `, }); - -export const serveConfig = defineServeConfig({ - port: args.getPort(), - fusiongraph: args.get('fusiongraph'), -}); diff --git a/e2e/cjs-project/mesh.config.ts b/e2e/cjs-project/mesh.config.ts index 8cfd75096576e..c7fcf5dc6e93f 100644 --- a/e2e/cjs-project/mesh.config.ts +++ b/e2e/cjs-project/mesh.config.ts @@ -1,16 +1,7 @@ const { GraphQLSchema, GraphQLObjectType, GraphQLString } = require('graphql'); -const { defineConfig: defineComposeConfig } = require('@graphql-mesh/compose-cli'); -const { defineConfig: defineServeConfig } = require('@graphql-mesh/serve-cli'); +const { defineConfig } = require('@graphql-mesh/compose-cli'); -const args = require('@e2e/args').Args(process.argv); - -const serveConfig = defineServeConfig({ - port: args.getPort(), - fusiongraph: '', -}); - -const composeConfig = defineComposeConfig({ - port: args.get('target'), +const composeConfig = defineConfig({ subgraphs: [ { sourceHandler: () => ({ @@ -31,4 +22,4 @@ const composeConfig = defineComposeConfig({ ], }); -module.exports = { serveConfig, composeConfig }; +module.exports = { composeConfig }; diff --git a/e2e/compose-to-target/__snapshots__/compose-to-target.test.ts.snap b/e2e/compose-to-output/__snapshots__/compose-to-output.test.ts.snap similarity index 100% rename from e2e/compose-to-target/__snapshots__/compose-to-target.test.ts.snap rename to e2e/compose-to-output/__snapshots__/compose-to-output.test.ts.snap diff --git a/e2e/compose-to-output/compose-to-output.test.ts b/e2e/compose-to-output/compose-to-output.test.ts new file mode 100644 index 0000000000000..3eb599ab5c3e0 --- /dev/null +++ b/e2e/compose-to-output/compose-to-output.test.ts @@ -0,0 +1,23 @@ +import { createTenv } from '@e2e/tenv'; + +const { compose, fs } = createTenv(__dirname); + +it('should write compose output to fusiongraph.graphql', async () => { + const { output } = await compose({ output: 'graphql' }); + await expect(fs.read(output)).resolves.toMatchSnapshot(); +}); + +it('should write compose output to fusiongraph.json', async () => { + const { output } = await compose({ output: 'json' }); + await expect(fs.read(output)).resolves.toMatchSnapshot(); +}); + +it('should write compose output to fusiongraph.js', async () => { + const { output } = await compose({ output: 'js' }); + await expect(fs.read(output)).resolves.toMatchSnapshot(); +}); + +it('should write compose output to fusiongraph.ts', async () => { + const { output } = await compose({ output: 'ts' }); + await expect(fs.read(output)).resolves.toMatchSnapshot(); +}); diff --git a/e2e/compose-to-target/mesh.config.ts b/e2e/compose-to-output/mesh.config.ts similarity index 87% rename from e2e/compose-to-target/mesh.config.ts rename to e2e/compose-to-output/mesh.config.ts index f5f3c8a2d9be4..6202788635edc 100644 --- a/e2e/compose-to-target/mesh.config.ts +++ b/e2e/compose-to-output/mesh.config.ts @@ -1,9 +1,7 @@ import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; -import { Args } from '@e2e/args'; import { defineConfig } from '@graphql-mesh/compose-cli'; export const composeConfig = defineConfig({ - target: Args(process.argv).get('target', true), subgraphs: [ { sourceHandler: () => ({ diff --git a/e2e/compose-to-target/package.json b/e2e/compose-to-output/package.json similarity index 83% rename from e2e/compose-to-target/package.json rename to e2e/compose-to-output/package.json index 269857d73e09e..30ab947bf6fcd 100644 --- a/e2e/compose-to-target/package.json +++ b/e2e/compose-to-output/package.json @@ -1,5 +1,5 @@ { - "name": "@e2e/compose-to-target", + "name": "@e2e/compose-to-output", "type": "module", "private": true, "dependencies": { diff --git a/e2e/compose-to-target/compose-to-target.test.ts b/e2e/compose-to-target/compose-to-target.test.ts deleted file mode 100644 index e26d1c9a1947e..0000000000000 --- a/e2e/compose-to-target/compose-to-target.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createTenv } from '@e2e/tenv'; - -const { compose, fs } = createTenv(__dirname); - -it('should write compose output to fusiongraph.graphql', async () => { - const { target } = await compose({ target: 'graphql' }); - await expect(fs.read(target)).resolves.toMatchSnapshot(); -}); - -it('should write compose output to fusiongraph.json', async () => { - const { target } = await compose({ target: 'json' }); - await expect(fs.read(target)).resolves.toMatchSnapshot(); -}); - -it('should write compose output to fusiongraph.js', async () => { - const { target } = await compose({ target: 'js' }); - await expect(fs.read(target)).resolves.toMatchSnapshot(); -}); - -it('should write compose output to fusiongraph.ts', async () => { - const { target } = await compose({ target: 'ts' }); - await expect(fs.read(target)).resolves.toMatchSnapshot(); -}); diff --git a/e2e/esm-config-in-cjs-project/mesh.config.ts b/e2e/esm-config-in-cjs-project/mesh.config.ts index e80bd2173b1d3..6202788635edc 100644 --- a/e2e/esm-config-in-cjs-project/mesh.config.ts +++ b/e2e/esm-config-in-cjs-project/mesh.config.ts @@ -1,17 +1,7 @@ import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; -import { Args } from '@e2e/args'; -import { defineConfig as defineComposeConfig } from '@graphql-mesh/compose-cli'; -import { defineConfig as defineServeConfig } from '@graphql-mesh/serve-cli'; +import { defineConfig } from '@graphql-mesh/compose-cli'; -const args = Args(process.argv); - -export const serveConfig = defineServeConfig({ - port: args.getPort(), - fusiongraph: '', -}); - -export const composeConfig = defineComposeConfig({ - target: args.get('target'), +export const composeConfig = defineConfig({ subgraphs: [ { sourceHandler: () => ({ diff --git a/e2e/esm-project/mesh.config.ts b/e2e/esm-project/mesh.config.ts index e80bd2173b1d3..6202788635edc 100644 --- a/e2e/esm-project/mesh.config.ts +++ b/e2e/esm-project/mesh.config.ts @@ -1,17 +1,7 @@ import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; -import { Args } from '@e2e/args'; -import { defineConfig as defineComposeConfig } from '@graphql-mesh/compose-cli'; -import { defineConfig as defineServeConfig } from '@graphql-mesh/serve-cli'; +import { defineConfig } from '@graphql-mesh/compose-cli'; -const args = Args(process.argv); - -export const serveConfig = defineServeConfig({ - port: args.getPort(), - fusiongraph: '', -}); - -export const composeConfig = defineComposeConfig({ - target: args.get('target'), +export const composeConfig = defineConfig({ subgraphs: [ { sourceHandler: () => ({ diff --git a/e2e/federation-example/mesh.config.ts b/e2e/federation-example/mesh.config.ts deleted file mode 100644 index c23df343eefb4..0000000000000 --- a/e2e/federation-example/mesh.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Args } from '@e2e/args'; -import { defineConfig } from '@graphql-mesh/serve-cli'; - -const args = Args(process.argv); - -export const serveConfig = defineConfig({ - port: args.getPort(), - supergraph: args.get('supergraph'), -}); diff --git a/e2e/json-schema-subscriptions/json-schema-subscriptions.test.ts b/e2e/json-schema-subscriptions/json-schema-subscriptions.test.ts index c75c4042819a5..9f2cb77c3afb2 100644 --- a/e2e/json-schema-subscriptions/json-schema-subscriptions.test.ts +++ b/e2e/json-schema-subscriptions/json-schema-subscriptions.test.ts @@ -10,8 +10,8 @@ it('should compose the appropriate schema', async () => { it('should query, mutate and subscribe', async () => { const servePort = await getAvailablePort(); const api = await service('api', { servePort }); - const { target } = await compose({ target: 'graphql', services: [api] }); - const { execute } = await serve({ fusiongraph: target, port: servePort }); + const { output } = await compose({ output: 'graphql', services: [api] }); + const { execute } = await serve({ fusiongraph: output, port: servePort }); await expect( execute({ diff --git a/e2e/json-schema-subscriptions/mesh.config.ts b/e2e/json-schema-subscriptions/mesh.config.ts index 501a5826ee943..54a6ebe0bf4de 100644 --- a/e2e/json-schema-subscriptions/mesh.config.ts +++ b/e2e/json-schema-subscriptions/mesh.config.ts @@ -9,7 +9,6 @@ import { loadJSONSchemaSubgraph } from '@omnigraph/json-schema'; const args = Args(process.argv); export const composeConfig = defineComposeConfig({ - target: args.get('target'), subgraphs: [ { sourceHandler: loadJSONSchemaSubgraph('API', { @@ -49,8 +48,7 @@ export const composeConfig = defineComposeConfig({ }); export const serveConfig = defineServeConfig({ - port: args.getPort(), - fusiongraph: args.get('fusiongraph'), + fusiongraph: '', // TODO: dont require fusiongraph option since it can be provided from as a CLI arg pubsub: new PubSub(), plugins: ctx => [ useWebhooks(ctx), diff --git a/e2e/logs-to-stderr-results-to-stdout/__snapshots__/logs-to-stderr-results-to-stdout.test.ts.snap b/e2e/logs-to-stderr-results-to-stdout/__snapshots__/logs-to-stderr-results-to-stdout.test.ts.snap index dacb433f90b85..16a8ebf9d4d38 100644 --- a/e2e/logs-to-stderr-results-to-stdout/__snapshots__/logs-to-stderr-results-to-stdout.test.ts.snap +++ b/e2e/logs-to-stderr-results-to-stdout/__snapshots__/logs-to-stderr-results-to-stdout.test.ts.snap @@ -10,41 +10,3 @@ type Query { } " `; - -exports[`should write compose output to stdout and logs to stderr 2`] = ` -"- Starting Mesh Compose CLI -- Starting Mesh Compose CLI -- Loading Mesh Compose CLI Config from e2e/logs-to-stderr-results-to-stdout/mesh.config.ts -- Starting Mesh Compose CLI -- Loaded Mesh Compose CLI Config from e2e/logs-to-stderr-results-to-stdout/mesh.config.ts -- Starting Mesh Compose CLI -- Loaded Mesh Compose CLI Config from e2e/logs-to-stderr-results-to-stdout/mesh.config.ts -- Loading subgraph helloworld -- Starting Mesh Compose CLI -- Loaded Mesh Compose CLI Config from e2e/logs-to-stderr-results-to-stdout/mesh.config.ts -- Loaded subgraph helloworld -- Starting Mesh Compose CLI -- Loaded Mesh Compose CLI Config from e2e/logs-to-stderr-results-to-stdout/mesh.config.ts -- Loaded subgraph helloworld -- Composing fusiongraph -- Starting Mesh Compose CLI -- Loaded Mesh Compose CLI Config from e2e/logs-to-stderr-results-to-stdout/mesh.config.ts -- Loaded subgraph helloworld -- Composed fusiongraph -- Starting Mesh Compose CLI -- Loaded Mesh Compose CLI Config from e2e/logs-to-stderr-results-to-stdout/mesh.config.ts -- Loaded subgraph helloworld -- Composed fusiongraph -- Writing Fusiongraph -- Starting Mesh Compose CLI -- Loaded Mesh Compose CLI Config from e2e/logs-to-stderr-results-to-stdout/mesh.config.ts -- Loaded subgraph helloworld -- Composed fusiongraph -- Written fusiongraph to stdout -- Finished Mesh Compose CLI -- Loaded Mesh Compose CLI Config from e2e/logs-to-stderr-results-to-stdout/mesh.config.ts -- Loaded subgraph helloworld -- Composed fusiongraph -- Written fusiongraph to stdout -" -`; diff --git a/e2e/logs-to-stderr-results-to-stdout/logs-to-stderr-results-to-stdout.test.ts b/e2e/logs-to-stderr-results-to-stdout/logs-to-stderr-results-to-stdout.test.ts index 7d68d12add357..182e760679d87 100644 --- a/e2e/logs-to-stderr-results-to-stdout/logs-to-stderr-results-to-stdout.test.ts +++ b/e2e/logs-to-stderr-results-to-stdout/logs-to-stderr-results-to-stdout.test.ts @@ -7,12 +7,12 @@ it('should write serve logs to stderr', async () => { await dispose(); expect(getStd('out')).toBeFalsy(); - expect(getStd('err')).toContain('Started server on'); + expect(getStd('err')).toContain('Starting server on'); }); it('should write compose output to stdout and logs to stderr', async () => { const { getStd } = await compose(); expect(getStd('out')).toMatchSnapshot(); - expect(getStd('err')).toMatchSnapshot(); + expect(getStd('err')).toContain('Done!'); }); diff --git a/e2e/logs-to-stderr-results-to-stdout/mesh.config.ts b/e2e/logs-to-stderr-results-to-stdout/mesh.config.ts index e80bd2173b1d3..6202788635edc 100644 --- a/e2e/logs-to-stderr-results-to-stdout/mesh.config.ts +++ b/e2e/logs-to-stderr-results-to-stdout/mesh.config.ts @@ -1,17 +1,7 @@ import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; -import { Args } from '@e2e/args'; -import { defineConfig as defineComposeConfig } from '@graphql-mesh/compose-cli'; -import { defineConfig as defineServeConfig } from '@graphql-mesh/serve-cli'; +import { defineConfig } from '@graphql-mesh/compose-cli'; -const args = Args(process.argv); - -export const serveConfig = defineServeConfig({ - port: args.getPort(), - fusiongraph: '', -}); - -export const composeConfig = defineComposeConfig({ - target: args.get('target'), +export const composeConfig = defineConfig({ subgraphs: [ { sourceHandler: () => ({ diff --git a/e2e/mysql-employees/mesh.config.ts b/e2e/mysql-employees/mesh.config.ts index 2485fbdc1e6dd..8b34c5c5b3efa 100644 --- a/e2e/mysql-employees/mesh.config.ts +++ b/e2e/mysql-employees/mesh.config.ts @@ -1,12 +1,10 @@ import { Args } from '@e2e/args'; -import { defineConfig as defineComposeConfig } from '@graphql-mesh/compose-cli'; -import { defineConfig as defineServeConfig } from '@graphql-mesh/serve-cli'; +import { defineConfig } from '@graphql-mesh/compose-cli'; import { loadMySQLSubgraph } from '@omnigraph/mysql'; const args = Args(process.argv); -export const composeConfig = defineComposeConfig({ - target: args.get('target'), +export const composeConfig = defineConfig({ subgraphs: [ { sourceHandler: loadMySQLSubgraph('Employees', { @@ -15,8 +13,3 @@ export const composeConfig = defineComposeConfig({ }, ], }); - -export const serveConfig = defineServeConfig({ - port: args.getPort(), - fusiongraph: args.get('fusiongraph'), -}); diff --git a/e2e/mysql-employees/mysql-employees.test.ts b/e2e/mysql-employees/mysql-employees.test.ts index 58192f980b727..2f8893ed42306 100644 --- a/e2e/mysql-employees/mysql-employees.test.ts +++ b/e2e/mysql-employees/mysql-employees.test.ts @@ -59,7 +59,7 @@ it.concurrent.each([ `, }, ])('should execute $name', async ({ query }) => { - const { target } = await compose({ target: 'graphql', services: [mysql] }); - const { execute } = await serve({ fusiongraph: target }); + const { output } = await compose({ output: 'graphql', services: [mysql] }); + const { execute } = await serve({ fusiongraph: output }); await expect(execute({ query })).resolves.toMatchSnapshot(); }); diff --git a/e2e/mysql-rfam/mesh.config.ts b/e2e/mysql-rfam/mesh.config.ts index a7b5ac910a08e..2e6c1468f31cc 100644 --- a/e2e/mysql-rfam/mesh.config.ts +++ b/e2e/mysql-rfam/mesh.config.ts @@ -1,13 +1,9 @@ -import { Args } from '@e2e/args'; import { defineConfig as defineComposeConfig } from '@graphql-mesh/compose-cli'; import { defineConfig as defineServeConfig } from '@graphql-mesh/serve-cli'; import { PubSub } from '@graphql-mesh/utils'; import { loadMySQLSubgraph } from '@omnigraph/mysql'; -const args = Args(process.argv); - export const composeConfig = defineComposeConfig({ - target: args.get('target'), subgraphs: [ { sourceHandler: loadMySQLSubgraph('Rfam', { @@ -18,7 +14,6 @@ export const composeConfig = defineComposeConfig({ }); export const serveConfig = defineServeConfig({ - port: args.getPort(), - fusiongraph: args.get('fusiongraph'), + fusiongraph: '', // TODO: dont require fusiongraph option since it can be provided from as a CLI arg pubsub: new PubSub(), }); diff --git a/e2e/mysql-rfam/mysql-rfam.test.ts b/e2e/mysql-rfam/mysql-rfam.test.ts index 22b1e584e9a9d..7bc85f4edb401 100644 --- a/e2e/mysql-rfam/mysql-rfam.test.ts +++ b/e2e/mysql-rfam/mysql-rfam.test.ts @@ -25,7 +25,7 @@ it.concurrent.each([ `, }, ])('should execute $name', async ({ query }) => { - const { target } = await compose({ target: 'graphql' }); - const { execute } = await serve({ fusiongraph: target }); + const { output } = await compose({ output: 'graphql' }); + const { execute } = await serve({ fusiongraph: output }); await expect(execute({ query })).resolves.toMatchSnapshot(); }); diff --git a/e2e/neo4j-example/mesh.config.ts b/e2e/neo4j-example/mesh.config.ts index 9ef3f263e8ff1..91fed65718954 100644 --- a/e2e/neo4j-example/mesh.config.ts +++ b/e2e/neo4j-example/mesh.config.ts @@ -1,13 +1,9 @@ -import { Args } from '@e2e/args'; import { defineConfig as defineComposeConfig } from '@graphql-mesh/compose-cli'; import { defineConfig as defineServeConfig } from '@graphql-mesh/serve-cli'; import { PubSub } from '@graphql-mesh/utils'; import { loadNeo4JSubgraph } from '@omnigraph/neo4j'; -const args = Args(process.argv); - export const composeConfig = defineComposeConfig({ - target: args.get('target'), subgraphs: [ { sourceHandler: loadNeo4JSubgraph('Movies', { @@ -24,7 +20,6 @@ export const composeConfig = defineComposeConfig({ }); export const serveConfig = defineServeConfig({ - port: args.getPort(), - fusiongraph: args.get('fusiongraph'), + fusiongraph: '', // TODO: dont require fusiongraph option since it can be provided from as a CLI arg pubsub: new PubSub(), }); diff --git a/e2e/neo4j-example/neo4j-example.test.ts b/e2e/neo4j-example/neo4j-example.test.ts index a0c3195bb295f..3748823225c3e 100644 --- a/e2e/neo4j-example/neo4j-example.test.ts +++ b/e2e/neo4j-example/neo4j-example.test.ts @@ -24,7 +24,7 @@ it.concurrent.each([ `, }, ])('should execute $name', async ({ query }) => { - const { target } = await compose({ target: 'graphql' }); - const { execute } = await serve({ fusiongraph: target }); + const { output } = await compose({ output: 'graphql' }); + const { execute } = await serve({ fusiongraph: output }); await expect(execute({ query })).resolves.toMatchSnapshot(); }); diff --git a/e2e/openapi-javascript-wiki/mesh.config.ts b/e2e/openapi-javascript-wiki/mesh.config.ts index a47a11379f1b5..56d5c7f368be0 100644 --- a/e2e/openapi-javascript-wiki/mesh.config.ts +++ b/e2e/openapi-javascript-wiki/mesh.config.ts @@ -1,13 +1,9 @@ import moment from 'moment'; -import { Args } from '@e2e/args'; import { defineConfig as defineComposeConfig } from '@graphql-mesh/compose-cli'; import { defineConfig as defineServeConfig } from '@graphql-mesh/serve-cli'; import { loadOpenAPISubgraph } from '@omnigraph/openapi'; -const args = Args(process.argv); - export const composeConfig = defineComposeConfig({ - target: args.get('target'), subgraphs: [ { sourceHandler: loadOpenAPISubgraph('Wiki', { @@ -25,8 +21,7 @@ export const composeConfig = defineComposeConfig({ }); export const serveConfig = defineServeConfig({ - port: args.getPort(), - fusiongraph: args.get('fusiongraph'), + fusiongraph: '', // TODO: dont require fusiongraph option since it can be provided from as a CLI arg additionalResolvers: { Query: { async viewsInPastMonth(root, { project }, context: any, info) { diff --git a/e2e/openapi-javascript-wiki/openapi-javascript-wiki.test.ts b/e2e/openapi-javascript-wiki/openapi-javascript-wiki.test.ts index f040d566159dd..8bbbef46317ed 100644 --- a/e2e/openapi-javascript-wiki/openapi-javascript-wiki.test.ts +++ b/e2e/openapi-javascript-wiki/openapi-javascript-wiki.test.ts @@ -38,7 +38,7 @@ it.concurrent.each([ `, }, ])('should execute $name', async ({ query }) => { - const { target } = await compose({ target: 'graphql' }); - const { execute } = await serve({ fusiongraph: target }); + const { output } = await compose({ output: 'graphql' }); + const { execute } = await serve({ fusiongraph: output }); await expect(execute({ query })).resolves.toMatchSnapshot(); }); diff --git a/e2e/openapi-subscriptions/mesh.config.ts b/e2e/openapi-subscriptions/mesh.config.ts index 8dc54b885d7bd..fae36ae38f2dc 100644 --- a/e2e/openapi-subscriptions/mesh.config.ts +++ b/e2e/openapi-subscriptions/mesh.config.ts @@ -7,7 +7,6 @@ import { loadOpenAPISubgraph } from '@omnigraph/openapi'; const args = Args(process.argv); export const composeConfig = defineComposeConfig({ - target: args.get('target'), subgraphs: [ { sourceHandler: loadOpenAPISubgraph('OpenAPICallbackExample', { @@ -19,8 +18,7 @@ export const composeConfig = defineComposeConfig({ }); export const serveConfig = defineServeConfig({ - port: args.getPort(), - fusiongraph: args.get('fusiongraph'), + fusiongraph: '', // TODO: dont require fusiongraph option since it can be provided from as a CLI arg pubsub: new PubSub(), plugins: ctx => [useWebhooks(ctx)], }); diff --git a/e2e/openapi-subscriptions/openapi-subscriptions.test.ts b/e2e/openapi-subscriptions/openapi-subscriptions.test.ts index 32e052dbe9674..5132dd2d3970e 100644 --- a/e2e/openapi-subscriptions/openapi-subscriptions.test.ts +++ b/e2e/openapi-subscriptions/openapi-subscriptions.test.ts @@ -11,8 +11,8 @@ it('should compose the appropriate schema', async () => { }); it('should listen for webhooks', async () => { - const { target } = await compose({ target: 'graphql', services: [await service('api')] }); - const { execute, port } = await serve({ fusiongraph: target }); + const { output } = await compose({ output: 'graphql', services: [await service('api')] }); + const { execute, port } = await serve({ fusiongraph: output }); const res = await execute({ query: /* GraphQL */ ` diff --git a/e2e/pubsub-destroy/mesh.config.ts b/e2e/pubsub-destroy/mesh.config.ts index a37db55d3ae15..c2084cd25a18a 100644 --- a/e2e/pubsub-destroy/mesh.config.ts +++ b/e2e/pubsub-destroy/mesh.config.ts @@ -1,10 +1,7 @@ import { createServer } from 'http'; -import { Args } from '@e2e/args'; import { defineConfig } from '@graphql-mesh/serve-cli'; import { PubSub } from '@graphql-mesh/utils'; -const args = Args(process.argv); - const pubsub = new PubSub(); // start a server that doesnt close until the pubsub is destroyed @@ -14,7 +11,6 @@ server.listen(); pubsub.subscribe('destroy', () => server.close()); export const serveConfig = defineConfig({ - port: args.getPort(), - fusiongraph: '', + fusiongraph: '', // TODO: dont require fusiongraph option since it can be provided from as a CLI arg pubsub, }); diff --git a/e2e/soap-demo/mesh.config.ts b/e2e/soap-demo/mesh.config.ts index 1081a5224de54..7603f43ac8ed7 100644 --- a/e2e/soap-demo/mesh.config.ts +++ b/e2e/soap-demo/mesh.config.ts @@ -1,12 +1,7 @@ -import { Args } from '@e2e/args'; -import { defineConfig as defineComposeConfig } from '@graphql-mesh/compose-cli'; -import { defineConfig as defineServeConfig } from '@graphql-mesh/serve-cli'; +import { defineConfig } from '@graphql-mesh/compose-cli'; import { loadSOAPSubgraph } from '@omnigraph/soap'; -const args = Args(process.argv); - -export const composeConfig = defineComposeConfig({ - target: args.get('target'), +export const composeConfig = defineConfig({ subgraphs: [ { sourceHandler: loadSOAPSubgraph('soap-demo', { @@ -15,8 +10,3 @@ export const composeConfig = defineComposeConfig({ }, ], }); - -export const serveConfig = defineServeConfig({ - port: args.getPort(), - fusiongraph: args.get('fusiongraph'), -}); diff --git a/e2e/soap-demo/soap-demo.test.ts b/e2e/soap-demo/soap-demo.test.ts index cf8582f3669d7..463cd66c2ea1d 100644 --- a/e2e/soap-demo/soap-demo.test.ts +++ b/e2e/soap-demo/soap-demo.test.ts @@ -76,7 +76,7 @@ it.concurrent.each([ `, }, ])('should execute $name', async ({ query }) => { - const { target } = await compose({ target: 'graphql' }); - const { execute } = await serve({ fusiongraph: target }); + const { output } = await compose({ output: 'graphql' }); + const { execute } = await serve({ fusiongraph: output }); await expect(execute({ query })).resolves.toMatchSnapshot(); }); diff --git a/e2e/utils/args.test.ts b/e2e/utils/args.test.ts index 58a78ce7ad367..393e89cc1f2e7 100644 --- a/e2e/utils/args.test.ts +++ b/e2e/utils/args.test.ts @@ -2,9 +2,9 @@ import { Args, createArg, createPortArg, createServicePortArg } from './args'; it.each([ { - key: 'target', + key: 'output', val: 'internet', - out: '--target=internet', + out: '--output=internet', }, { key: 'port', @@ -54,8 +54,8 @@ it.each([ it.each([ { - argv: ['yarn', 'mesh', createArg('target', 'internet')], - key: 'target', + argv: ['yarn', 'mesh', createArg('output', 'internet')], + key: 'output', val: 'internet', }, ])('should get str "$val" by "$key" from $argv', ({ argv, key, val }) => { @@ -74,7 +74,7 @@ it.each([ it.each([ { argv: ['yarn', 'mesh'], - key: 'target', + key: 'output', }, ])('should get undefined by "$key" from $argv', ({ argv, key }) => { expect(Args(argv).get(key)).toBeUndefined(); @@ -83,11 +83,11 @@ it.each([ it.each([ { argv: ['yarn', 'mesh'], - key: 'target', + key: 'output', }, { - argv: ['yarn', 'mesh', '--target space=internet'], - key: 'target space', + argv: ['yarn', 'mesh', '--output space=internet'], + key: 'output space', }, ])('should throw when requiring "$key" from $argv', ({ argv, key }) => { expect(() => Args(argv).get(key, true)).toThrow(); diff --git a/e2e/utils/tenv.ts b/e2e/utils/tenv.ts index dc180fc675c83..972027e4f0f1f 100644 --- a/e2e/utils/tenv.ts +++ b/e2e/utils/tenv.ts @@ -91,7 +91,7 @@ export interface ComposeOptions extends ProcOptions { * Write the compose output/result to a temporary unique file with the extension. * The file will be deleted after the tests complete. */ - target?: 'graphql' | 'json' | 'js' | 'ts'; + output?: 'graphql' | 'json' | 'js' | 'ts'; /** * Services relevant to the compose process. * It will supply `--_port=` arguments to the process. @@ -105,10 +105,10 @@ export interface ComposeOptions extends ProcOptions { export interface Compose extends Proc { /** - * The path to the target composed file. - * If target was not specified in the options, an empty string will be provided. + * The path to the composed file. + * If output was not specified in the options, an empty string will be provided. */ - target: string; + output: string; result: string; } @@ -227,11 +227,11 @@ export function createTenv(cwd: string): Tenv { }, async compose(opts) { const { services = [], trimHostPaths, maskServicePorts, pipeLogs } = opts || {}; - let target = ''; - if (opts?.target) { + let output = ''; + if (opts?.output) { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'graphql-mesh_e2e_compose')); leftovers.add(tempDir); - target = path.join(tempDir, `${Math.random().toString(32).slice(2)}.${opts.target}`); + output = path.join(tempDir, `${Math.random().toString(32).slice(2)}.${opts.output}`); } const [proc, waitForExit] = await spawn( { cwd, pipeLogs }, @@ -239,18 +239,18 @@ export function createTenv(cwd: string): Tenv { '--import', 'tsx', path.resolve(__project, 'packages', 'compose-cli', 'src', 'bin.ts'), - target && createArg('target', target), + output && createArg('output', output), ...services.map(({ name, port }) => createServicePortArg(name, port)), ); await waitForExit; let result = ''; - if (target) { + if (output) { try { - result = await fs.readFile(target, 'utf-8'); + result = await fs.readFile(output, 'utf-8'); } catch (err) { if ('code' in err && err.code === 'ENOENT') { throw new Error( - `Compose command has "target" argument but file was not created at ${target}`, + `Compose command has "output" argument but file was not created at ${output}`, ); } throw err; @@ -268,12 +268,12 @@ export function createTenv(cwd: string): Tenv { result = result.replaceAll(subgraph.port.toString(), `<${subgraph.name}_port>`); } } - if (target) { - await fs.writeFile(target, result, 'utf8'); + if (output) { + await fs.writeFile(output, result, 'utf8'); } } - return { ...proc, target, result }; + return { ...proc, output, result }; }, async service(name, { port, servePort, pipeLogs } = {}) { port ||= await getAvailablePort(); diff --git a/packages/compose-cli/package.json b/packages/compose-cli/package.json index b1007a76862ce..be5158b39375d 100644 --- a/packages/compose-cli/package.json +++ b/packages/compose-cli/package.json @@ -39,19 +39,17 @@ "graphql": "*" }, "dependencies": { + "@commander-js/extra-typings": "^12.0.1", "@graphql-mesh/fusion-composition": "^0.0.2", "@graphql-mesh/utils": "^0.97.5", "@graphql-tools/graphql-file-loader": "8.0.1", "@graphql-tools/load": "^8.0.1", "@graphql-tools/utils": "^10.0.8", "@whatwg-node/fetch": "^0.9.14", + "commander": "^12.0.0", "dotenv": "^16.3.1", - "spinnies": "^0.5.1", "tsx": "^4.7.1" }, - "devDependencies": { - "@types/spinnies": "^0.5.3" - }, "publishConfig": { "access": "public", "directory": "dist" diff --git a/packages/compose-cli/src/bin.ts b/packages/compose-cli/src/bin.ts index b0ea04a622485..c65277bee9d9d 100644 --- a/packages/compose-cli/src/bin.ts +++ b/packages/compose-cli/src/bin.ts @@ -1,10 +1,10 @@ #!/usr/bin/env node -import { runComposeCLI, spinnies } from './runComposeCLI.js'; -import 'dotenv/config'; -import 'tsx/cjs'; +import { DefaultLogger } from '@graphql-mesh/utils'; +import { run } from './run.js'; -runComposeCLI().catch(e => { - spinnies.stopAll('fail'); - console.error(e); +const log = new DefaultLogger(); + +run({ log }).catch(err => { + log.error(err); process.exit(1); }); diff --git a/packages/compose-cli/src/getComposedSchemaFromConfig.ts b/packages/compose-cli/src/getComposedSchemaFromConfig.ts index c432227c2468b..b2b0be7c5e808 100644 --- a/packages/compose-cli/src/getComposedSchemaFromConfig.ts +++ b/packages/compose-cli/src/getComposedSchemaFromConfig.ts @@ -1,31 +1,28 @@ import { DocumentNode, GraphQLSchema } from 'graphql'; import { composeSubgraphs, SubgraphConfig } from '@graphql-mesh/fusion-composition'; -import { DefaultLogger } from '@graphql-mesh/utils'; +import { Logger } from '@graphql-mesh/types'; import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; import { loadTypedefs } from '@graphql-tools/load'; import { fetch as defaultFetch } from '@whatwg-node/fetch'; import { LoaderContext, MeshComposeCLIConfig } from './types.js'; -export async function getComposedSchemaFromConfig( - meshComposeCLIConfig: MeshComposeCLIConfig, - spinnies?: Spinnies, -) { +export async function getComposedSchemaFromConfig(config: MeshComposeCLIConfig, logger: Logger) { const ctx: LoaderContext = { - fetch: meshComposeCLIConfig.fetch || defaultFetch, - cwd: meshComposeCLIConfig.cwd || globalThis.process?.cwd?.(), - logger: new DefaultLogger(), + fetch: config.fetch || defaultFetch, + cwd: config.cwd || globalThis.process?.cwd?.(), + logger, }; const subgraphConfigsForComposition: SubgraphConfig[] = await Promise.all( - meshComposeCLIConfig.subgraphs.map(async subgraphCLIConfig => { + config.subgraphs.map(async subgraphCLIConfig => { const { name: subgraphName, schema$ } = subgraphCLIConfig.sourceHandler(ctx); - spinnies?.add(subgraphName, { text: `Loading subgraph ${subgraphName}` }); + const log = logger.child(`"${subgraphName}" subgraph`); + log.info(`Loading`); let subgraphSchema: GraphQLSchema; try { subgraphSchema = await schema$; } catch (e) { throw new Error(`Failed to load subgraph ${subgraphName} - ${e.stack}`); } - spinnies?.succeed(subgraphName, { text: `Loaded subgraph ${subgraphName}` }); return { name: subgraphName, schema: subgraphSchema, @@ -33,10 +30,9 @@ export async function getComposedSchemaFromConfig( }; }), ); - spinnies?.add('composition', { text: `Composing fusiongraph` }); let additionalTypeDefs: (DocumentNode | string)[] | undefined; - if (meshComposeCLIConfig.additionalTypeDefs != null) { - const result = await loadTypedefs(meshComposeCLIConfig.additionalTypeDefs, { + if (config.additionalTypeDefs != null) { + const result = await loadTypedefs(config.additionalTypeDefs, { noLocation: true, assumeValid: true, assumeValidSDL: true, @@ -47,13 +43,11 @@ export async function getComposedSchemaFromConfig( let composedSchema = composeSubgraphs(subgraphConfigsForComposition, { typeDefs: additionalTypeDefs, }); - if (meshComposeCLIConfig.transforms?.length) { - spinnies?.add('transforms', { text: `Applying transforms` }); - for (const transform of meshComposeCLIConfig.transforms) { + if (config.transforms?.length) { + logger.info('Applying transforms'); + for (const transform of config.transforms) { composedSchema = transform(composedSchema); } - spinnies?.succeed('transforms', { text: `Applied transforms` }); } - spinnies?.succeed('composition', { text: `Composed fusiongraph` }); return composedSchema; } diff --git a/packages/compose-cli/src/index.ts b/packages/compose-cli/src/index.ts index de4af1516dc40..4ca506bfda94f 100644 --- a/packages/compose-cli/src/index.ts +++ b/packages/compose-cli/src/index.ts @@ -1,4 +1,4 @@ -export * from './runComposeCLI.js'; +export * from './run.js'; export * from './types.js'; export * from './loadGraphQLHTTPSubgraph.js'; export * from '@graphql-mesh/fusion-composition'; diff --git a/packages/compose-cli/src/run.ts b/packages/compose-cli/src/run.ts new file mode 100644 index 0000000000000..03e33a5e71d1b --- /dev/null +++ b/packages/compose-cli/src/run.ts @@ -0,0 +1,119 @@ +import 'tsx/cjs'; // support importing typescript configs +import 'dotenv/config'; // inject dotenv options to process.env + +// eslint-disable-next-line import/no-nodejs-modules +import { promises as fsPromises } from 'fs'; +// eslint-disable-next-line import/no-nodejs-modules +import { isAbsolute, join, resolve } from 'path'; +import { parse } from 'graphql'; +import { Command, Option } from '@commander-js/extra-typings'; +import { Logger } from '@graphql-mesh/types'; +import { DefaultLogger } from '@graphql-mesh/utils'; +import { printSchemaWithDirectives } from '@graphql-tools/utils'; +import { getComposedSchemaFromConfig } from './getComposedSchemaFromConfig.js'; +import { MeshComposeCLIConfig } from './types.js'; + +let program = new Command() + .addOption( + new Option('-c, --config-path ', 'path to the configuration file') + .env('CONFIG_PATH') + .default('mesh.config.ts'), + ) + .option('-o, --output ', 'path to the output file'); + +export interface RunOptions extends ReturnType { + /** @default new DefaultLogger() */ + log?: Logger; + /** @default Mesh Compose */ + productName?: string; + /** @default compose a GraphQL federated schema from any API service(s) */ + productDescription?: string; + /** @default mesh-compose */ + binName?: string; + /** @default undefined */ + version?: string; +} + +export async function run({ + log: rootLog = new DefaultLogger(), + productName = 'Mesh Compose', + productDescription = 'compose a GraphQL federated schema from any API service(s)', + binName = 'mesh-compose', + version, +}: RunOptions): Promise { + program = program.name(binName).description(productDescription); + if (version) program = program.version(version); + if (process.env.NODE_ENV === 'test') program = program.allowUnknownOption(); + const opts = program.parse().opts(); + + const log = rootLog.child(`🕸️ ${productName}`); + + const configPath = isAbsolute(opts.configPath) + ? opts.configPath + : resolve(process.cwd(), opts.configPath); + log.info(`Checking configuration at ${configPath}`); + const importedConfig: { composeConfig?: MeshComposeCLIConfig } = await import(configPath).catch( + err => { + if (err.code === 'ERR_MODULE_NOT_FOUND') { + return {}; // no config is ok + } + log.error('Loading configuration failed!'); + throw err; + }, + ); + if (importedConfig.composeConfig) { + log.info('Loaded configuration'); + } else { + throw new Error(`No configuration found at ${configPath}`); + } + + const config: MeshComposeCLIConfig = { + ...importedConfig?.composeConfig, + ...opts, + }; + + log.info('Composing'); + + const fusiongraphSchema = await getComposedSchemaFromConfig(config, log); + const fusiongraph = printSchemaWithDirectives(fusiongraphSchema); + + let output = config.output; + if (!output) { + if (typeof process === 'object') { + process.stdout.write(fusiongraph + '\n'); + } else { + console.log(fusiongraph); + } + log.info('Done!'); + return; + } + + log.info(`Writing schema to ${output}`); + + output = isAbsolute(output) ? output : join(process.cwd(), output); + let writtenData: string; + if (output.endsWith('.json')) { + writtenData = JSON.stringify(parse(fusiongraph, { noLocation: true }), null, 2); + } else if ( + output.endsWith('.graphql') || + output.endsWith('.gql') || + output.endsWith('.graphqls') || + output.endsWith('.gqls') + ) { + writtenData = fusiongraph; + } else if ( + output.endsWith('.ts') || + output.endsWith('.cts') || + output.endsWith('.mts') || + output.endsWith('.js') || + output.endsWith('.cjs') || + output.endsWith('.mjs') + ) { + writtenData = `export default ${JSON.stringify(fusiongraph)}`; + } else { + throw new Error(`Unsupported file extension for ${output}`); + } + await fsPromises.writeFile(output, writtenData, 'utf8'); + + log.info('Done!'); +} diff --git a/packages/compose-cli/src/runComposeCLI.ts b/packages/compose-cli/src/runComposeCLI.ts deleted file mode 100644 index 92ecbc85aa030..0000000000000 --- a/packages/compose-cli/src/runComposeCLI.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* eslint-disable import/no-nodejs-modules */ -import { promises as fsPromises } from 'fs'; -import { isAbsolute, join } from 'path'; -import { parse } from 'graphql'; -import Spinnies from 'spinnies'; -import { printSchemaWithDirectives } from '@graphql-tools/utils'; -import { getComposedSchemaFromConfig } from './getComposedSchemaFromConfig.js'; -import { MeshComposeCLIConfig } from './types.js'; - -export const spinnies = new Spinnies({ - color: 'white', - succeedColor: 'white', - failColor: 'red', - succeedPrefix: '✔', - failPrefix: '💥', - spinner: { interval: 80, frames: ['/', '|', '\\', '--'] }, -}); - -export interface RunComposeCLIOpts { - defaultConfigFileName?: string; - defaultConfigFilePath?: string; - productName?: string; - processExit?: (exitCode: number) => void; -} - -const defaultProcessExit = (exitCode: number) => process.exit(exitCode); - -export async function runComposeCLI({ - defaultConfigFileName = 'mesh.config.ts', - defaultConfigFilePath = process.cwd(), - productName = 'Mesh Compose CLI', - processExit = defaultProcessExit, -}: RunComposeCLIOpts = {}): Promise { - spinnies.add('main', { text: 'Starting ' + productName }); - const meshComposeCLIConfigFileName = - process.env.MESH_COMPOSE_CONFIG_FILE_NAME || defaultConfigFileName; - const meshComposeCLIConfigFilePath = - process.env.MESH_COMPOSE_CONFIG_FILE_PATH || - join(defaultConfigFilePath, meshComposeCLIConfigFileName); - spinnies.add('config', { - text: `Loading ${productName} Config from ${meshComposeCLIConfigFilePath}`, - }); - const loadedConfig: { composeConfig: MeshComposeCLIConfig } = await import( - meshComposeCLIConfigFilePath - ); - const meshComposeCLIConfig = loadedConfig.composeConfig; - if (!meshComposeCLIConfig) { - spinnies.fail('config', { - text: `${productName} Config was not found in ${meshComposeCLIConfigFilePath}`, - }); - return processExit(1); - } - spinnies.succeed('config', { - text: `Loaded ${productName} Config from ${meshComposeCLIConfigFilePath}`, - }); - - const composedSchema = await getComposedSchemaFromConfig(meshComposeCLIConfig, spinnies); - - spinnies.add('write', { text: `Writing Fusiongraph` }); - const printedSupergraph = printSchemaWithDirectives(composedSchema); - - const fusiongraphFileName = meshComposeCLIConfig.target; - - if (!fusiongraphFileName) { - if (typeof process === 'object') { - process.stdout.write(printedSupergraph + '\n'); - } else { - console.log(printedSupergraph); - } - spinnies.succeed('write', { text: 'Written fusiongraph to stdout' }); - spinnies.succeed('main', { text: 'Finished ' + productName }); - return; - } - - const fusiongraphPath = isAbsolute(fusiongraphFileName) - ? fusiongraphFileName - : join(process.cwd(), fusiongraphFileName); - - let writtenData: string; - if (fusiongraphPath.endsWith('.json')) { - writtenData = JSON.stringify(parse(printedSupergraph, { noLocation: true }), null, 2); - } else if ( - fusiongraphPath.endsWith('.graphql') || - fusiongraphPath.endsWith('.gql') || - fusiongraphPath.endsWith('.graphqls') || - fusiongraphPath.endsWith('.gqls') - ) { - writtenData = printedSupergraph; - } else if ( - fusiongraphPath.endsWith('.ts') || - fusiongraphPath.endsWith('.cts') || - fusiongraphPath.endsWith('.mts') || - fusiongraphPath.endsWith('.js') || - fusiongraphPath.endsWith('.cjs') || - fusiongraphPath.endsWith('.mjs') - ) { - writtenData = `export default ${JSON.stringify(printedSupergraph)}`; - } else { - console.error(`Unsupported file extension for ${fusiongraphPath}`); - return processExit(1); - } - await fsPromises.writeFile(fusiongraphPath, writtenData, 'utf8'); - spinnies.succeed('write', { text: `Written fusiongraph to ${fusiongraphPath}` }); - spinnies.succeed('main', { text: 'Finished ' + productName }); -} diff --git a/packages/compose-cli/src/types.ts b/packages/compose-cli/src/types.ts index 69a9888863348..d2884a62d38d8 100644 --- a/packages/compose-cli/src/types.ts +++ b/packages/compose-cli/src/types.ts @@ -3,10 +3,14 @@ import { Logger } from '@graphql-mesh/types'; import { fetch as defaultFetch } from '@whatwg-node/fetch'; export interface MeshComposeCLIConfig { + /** + * The output destination of the resulting composed GraphQL schema. + * By default, the CLI will write the result to stdout. + */ + output?: string; subgraphs: MeshComposeCLISubgraphConfig[]; transforms?: MeshComposeCLITransformConfig[]; additionalTypeDefs?: string | DocumentNode | (string | DocumentNode)[]; - target?: string; fetch?: typeof defaultFetch; cwd?: string; } diff --git a/packages/serve-cli/package.json b/packages/serve-cli/package.json index 7f2b65759383d..0ce241cdef75d 100644 --- a/packages/serve-cli/package.json +++ b/packages/serve-cli/package.json @@ -44,10 +44,13 @@ } }, "dependencies": { + "@commander-js/extra-typings": "^12.0.1", "@graphql-mesh/cross-helpers": "^0.4.1", "@graphql-mesh/serve-runtime": "^0.2.12", "@graphql-mesh/types": "^0.97.5", "@graphql-mesh/utils": "^0.97.5", + "@graphql-tools/utils": "^10.1.3", + "commander": "^12.0.0", "dotenv": "^16.3.1", "json-bigint-patch": "^0.0.8", "spinnies": "^0.5.1", diff --git a/packages/serve-cli/src/bin.ts b/packages/serve-cli/src/bin.ts index efe3f01d22778..c65277bee9d9d 100644 --- a/packages/serve-cli/src/bin.ts +++ b/packages/serve-cli/src/bin.ts @@ -1,10 +1,10 @@ #!/usr/bin/env node -import { runServeCLI } from './runServeCLI.js'; -import 'dotenv/config'; -import 'json-bigint-patch'; -import 'tsx/cjs'; +import { DefaultLogger } from '@graphql-mesh/utils'; +import { run } from './run.js'; -runServeCLI().catch(e => { - console.error(e); +const log = new DefaultLogger(); + +run({ log }).catch(err => { + log.error(err); process.exit(1); }); diff --git a/packages/serve-cli/src/index.ts b/packages/serve-cli/src/index.ts index 9623ddc4e7415..a0771740460d3 100644 --- a/packages/serve-cli/src/index.ts +++ b/packages/serve-cli/src/index.ts @@ -1,4 +1,4 @@ -export * from './runServeCLI.js'; +export * from './run.js'; export * from './types.js'; export { useWebhooks, diff --git a/packages/serve-cli/src/run.ts b/packages/serve-cli/src/run.ts new file mode 100644 index 0000000000000..f4a5e2b3d7f20 --- /dev/null +++ b/packages/serve-cli/src/run.ts @@ -0,0 +1,265 @@ +import 'json-bigint-patch'; // JSON.parse/stringify with bigints support +import 'tsx/cjs'; // support importing typescript configs +import 'dotenv/config'; // inject dotenv options to process.env + +// eslint-disable-next-line import/no-nodejs-modules +import cluster from 'cluster'; +// eslint-disable-next-line import/no-nodejs-modules +import { availableParallelism, release } from 'os'; +// eslint-disable-next-line import/no-nodejs-modules +import { dirname, isAbsolute, resolve } from 'path'; +import { App, SSLApp } from 'uWebSockets.js'; +import { Command, InvalidArgumentError, Option } from '@commander-js/extra-typings'; +import { createServeRuntime, UnifiedGraphConfig } from '@graphql-mesh/serve-runtime'; +import { Logger } from '@graphql-mesh/types'; +import { DefaultLogger, registerTerminateHandler } from '@graphql-mesh/utils'; +import { isValidPath } from '@graphql-tools/utils'; +import { MeshServeCLIConfig } from './types.js'; + +const defaultFork = process.env.NODE_ENV === 'production' ? availableParallelism() : 1; + +let program = new Command() + .addOption( + new Option( + '--fork [count]', + 'count of workers to spawn. defaults to `os.availableParallelism()` when NODE_ENV is "production", otherwise only one (the main) worker', + ) + .env('FORK') + .argParser(v => { + const count = parseInt(v); + if (isNaN(count)) { + throw new InvalidArgumentError('not a number.'); + } + return count; + }) + .default(defaultFork), + ) + .addOption( + new Option('-c, --config-path ', 'path to the configuration file') + .env('CONFIG_PATH') + .default('mesh.config.ts'), + ) + .option( + '-h, --host ', + 'host to use for serving', + release().toLowerCase().includes('microsoft') ? '127.0.0.1' : '0.0.0.0', + ) + .option( + '-p, --port ', + 'port to use for serving', + v => { + const port = parseInt(v); + if (isNaN(port)) { + throw new InvalidArgumentError('not a number.'); + } + return port; + }, + 4000, + ) + .addOption( + new Option('--fusiongraph ', 'path to the fusiongraph schema') + .conflicts('supergraph') + .default('fusiongraph.graphql'), + ) + .addOption( + new Option('--supergraph ', 'path to the supergraph schema').conflicts('fusiongraph'), + ); + +export interface RunOptions extends ReturnType { + /** @default new DefaultLogger() */ + log?: Logger; + /** @default Mesh Serve */ + productName?: string; + /** @default serve GraphQL federated architecture for any API service(s) */ + productDescription?: string; + /** @default mesh-serve */ + binName?: string; + /** @default undefined */ + version?: string; +} + +export async function run({ + log: rootLog = new DefaultLogger(), + productName = 'Mesh', + productDescription = 'serve GraphQL federated architecture for any API service(s)', + binName = 'mesh-serve', + version, +}: RunOptions) { + program = program.name(binName).description(productDescription); + if (version) program = program.version(version); + const opts = program.parse().opts(); + + const log = rootLog.child( + cluster.worker?.id ? `🕸️ ${productName} Worker#${cluster.worker.id}` : `🕸️ ${productName}`, + ); + + const configPath = isAbsolute(opts.configPath) + ? opts.configPath + : resolve(process.cwd(), opts.configPath); + log.info(`Checking configuration at ${configPath}`); + const importedConfig: { serveConfig?: MeshServeCLIConfig } = await import(configPath).catch( + err => { + if (err.code === 'ERR_MODULE_NOT_FOUND') { + return {}; // no config is ok + } + log.error('Loading configuration failed!'); + throw err; + }, + ); + if (importedConfig.serveConfig) { + log.info('Loaded configuration'); + } else { + log.info('No configuration found'); + } + + const config: MeshServeCLIConfig = { + ...importedConfig?.serveConfig, + ...opts, + }; + + if (config.pubsub) { + registerTerminateHandler(eventName => { + log.info(`Destroying pubsub for ${eventName}`); + config.pubsub!.publish('destroy', undefined); + }); + } + + let unifiedGraphPath: UnifiedGraphConfig; + let spec: 'federation' | 'fusion'; + + if ('supergraph' in config) { + unifiedGraphPath = config.supergraph; + spec = 'federation'; + // the program defaults to fusiongraph, remove it + // from the config if a supergraph is provided + // @ts-expect-error fusiongraph _can_ be in the config + delete config.fusiongraph; + } else if ('fusiongraph' in config) { + unifiedGraphPath = config.fusiongraph; + spec = 'fusion'; + } else if (!('http' in config)) { + unifiedGraphPath = './fusiongraph.graphql'; + } + + let loadingMessage: string; + switch (spec) { + case 'fusion': + if (typeof unifiedGraphPath === 'string') { + loadingMessage = `Loading Fusiongraph from ${unifiedGraphPath}`; + } else { + loadingMessage = `Loading Fusiongraph`; + } + break; + case 'federation': + if (typeof unifiedGraphPath === 'string') { + loadingMessage = `Loading Supergraph from ${unifiedGraphPath}`; + } else { + loadingMessage = `Loading Supergraph`; + } + break; + default: + if (typeof unifiedGraphPath === 'string') { + loadingMessage = `Loading schema from ${unifiedGraphPath}`; + } else { + loadingMessage = `Loading schema`; + } + } + + log.info(loadingMessage); + + const unifiedGraphName = spec === 'fusion' ? 'fusiongraph' : 'supergraph'; + + if (cluster.isPrimary) { + const fork = opts.fork === true ? defaultFork : opts.fork; + + if (isValidPath(unifiedGraphPath)) { + let watcher: typeof import('@parcel/watcher') | undefined; + try { + watcher = await import('@parcel/watcher'); + } catch (err) { + log.warn( + `If you want to enable hot reloading when ${unifiedGraphPath} changes, install "@parcel/watcher"`, + ); + } + if (watcher) { + try { + const absoluteUnifiedGraphPath = isAbsolute(String(unifiedGraphPath)) + ? String(unifiedGraphPath) + : resolve(process.cwd(), String(unifiedGraphPath)); + const absolutUnifiedGraphDir = dirname(absoluteUnifiedGraphPath); + const subscription = await watcher.subscribe(absolutUnifiedGraphDir, (err, events) => { + if (err) { + log.error(err); + return; + } + if (events.some(event => event.path === absoluteUnifiedGraphPath)) { + log.info(`${unifiedGraphName} changed`); + if (fork > 1) { + for (const workerId in cluster.workers) { + cluster.workers[workerId].send('invalidateUnifiedGraph'); + } + } else { + handler.invalidateUnifiedGraph(); + } + } + }); + registerTerminateHandler(eventName => { + log.info(`Closing watcher for ${absoluteUnifiedGraphPath} for ${eventName}`); + return subscription.unsubscribe(); + }); + } catch (err) { + log.error(`Failed to watch ${unifiedGraphPath}!`); + throw err; + } + } + } + + if (fork > 1) { + log.info(`Forking ${fork} ${productName} Workers`); + for (let i = 0; i < fork; i++) { + log.info(`Forking ${productName} Worker #${i}`); + const worker = cluster.fork(); + registerTerminateHandler(eventName => { + log.info(`Closing ${productName} Worker #${i} for ${eventName}`); + worker.kill(eventName); + log.info(`Closed ${productName} Worker #${i} for ${eventName}`); + }); + log.info(`Forked ${productName} Worker #${i}`); + } + log.info(`Forked ${fork} ${productName} Workers`); + return; + } + } + + const port = config.port!; + const host = config.host!; + const protocol = config.sslCredentials ? 'https' : 'http'; + + const handler = createServeRuntime({ + logging: log, + ...config, + }); + process.on('message', message => { + if (message === 'invalidateUnifiedGraph') { + log.info(`Invalidating ${unifiedGraphName}`); + handler.invalidateUnifiedGraph(); + } + }); + + await new Promise((resolve, reject) => { + log.info(`Starting server on ${protocol}://${host}:${port}`); + const app = config.sslCredentials ? SSLApp(config.sslCredentials) : App(); + app.any('/*', handler); + app.listen(host, port, function listenCallback(listenSocket) { + if (listenSocket) { + registerTerminateHandler(eventName => { + log.info(`Closing ${protocol}://${host}:${port} for ${eventName}`); + app.close(); + }); + resolve(); + } else { + reject(new Error(`Failed to start server on ${protocol}://${host}:${port}!`)); + } + }); + }); +} diff --git a/packages/serve-cli/src/runServeCLI.ts b/packages/serve-cli/src/runServeCLI.ts deleted file mode 100644 index 5da7f67db10de..0000000000000 --- a/packages/serve-cli/src/runServeCLI.ts +++ /dev/null @@ -1,216 +0,0 @@ -/* eslint-disable import/no-nodejs-modules */ -import cluster from 'cluster'; -import { availableParallelism, platform, release } from 'os'; -import { dirname, isAbsolute, join, relative } from 'path'; -import { App, SSLApp } from 'uWebSockets.js'; -import { createServeRuntime, UnifiedGraphConfig } from '@graphql-mesh/serve-runtime'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { DefaultLogger, registerTerminateHandler } from '@graphql-mesh/utils'; -import { MeshServeCLIConfig } from './types.js'; - -interface RunServeCLIOpts { - defaultConfigFileName?: string; - defaultConfigFilePath?: string; - productName?: string; - defaultConfig?: MeshServeCLIConfig; - processExit?: (exitCode: number) => void; -} - -const defaultProcessExit = (exitCode: number) => process.exit(exitCode); - -export async function runServeCLI({ - processExit = defaultProcessExit, - defaultConfigFileName = 'mesh.config.ts', - defaultConfigFilePath = process.cwd(), - defaultConfig = { - fusiongraph: './fusiongraph.graphql', - }, - productName = 'Mesh', -}: RunServeCLIOpts = {}): Promise { - const prefix = cluster.worker?.id - ? `🕸️ ${productName} Worker#${cluster.worker.id}` - : `🕸️ ${productName}`; - const workerLogger = new DefaultLogger(prefix); - workerLogger.info(`Starting`); - - const meshServeCLIConfigFileName = - process.env.MESH_SERVE_CONFIG_FILE_NAME || defaultConfigFileName; - const meshServeCLIConfigFilePath = - process.env.MESH_SERVE_CONFIG_FILE_PATH || - join(defaultConfigFilePath, meshServeCLIConfigFileName); - - const meshServeCLIConfigRelativePath = relative(process.cwd(), meshServeCLIConfigFilePath); - - workerLogger.info(`Loading configuration from ${meshServeCLIConfigRelativePath}`); - const loadedConfig: { serveConfig: MeshServeCLIConfig } = await import( - meshServeCLIConfigFilePath - ).catch(e => { - workerLogger.error(`Failed to load configuration from ${meshServeCLIConfigRelativePath}`, e); - return processExit(1); - }); - - const meshServeCLIConfig = loadedConfig.serveConfig || defaultConfig; - workerLogger.info(`Loaded configuration from ${meshServeCLIConfigRelativePath}`); - - if (meshServeCLIConfig.pubsub) { - registerTerminateHandler(eventName => { - workerLogger.info(`Destroying pubsub for ${eventName}`); - meshServeCLIConfig.pubsub.publish('destroy', undefined); - }); - } - - let unifiedGraphPath: UnifiedGraphConfig; - let spec: 'federation' | 'fusion'; - - if ('fusiongraph' in meshServeCLIConfig) { - unifiedGraphPath = meshServeCLIConfig.fusiongraph; - spec = 'fusion'; - } else if ('supergraph' in meshServeCLIConfig) { - unifiedGraphPath = meshServeCLIConfig.supergraph; - spec = 'federation'; - } else if (!('http' in meshServeCLIConfig)) { - unifiedGraphPath = './fusiongraph.graphql'; - } - - let loadingMessage: string; - switch (spec) { - case 'fusion': - if (typeof unifiedGraphPath === 'string') { - loadingMessage = `Loading Fusiongraph from ${unifiedGraphPath}`; - } else { - loadingMessage = `Loading Fusiongraph`; - } - break; - case 'federation': - if (typeof unifiedGraphPath === 'string') { - loadingMessage = `Loading Supergraph from ${unifiedGraphPath}`; - } else { - loadingMessage = `Loading Supergraph`; - } - break; - default: - if (typeof unifiedGraphPath === 'string') { - loadingMessage = `Loading schema from ${unifiedGraphPath}`; - } else { - loadingMessage = `Loading schema`; - } - } - - workerLogger.info(loadingMessage); - - const unifiedGraphName = spec === 'fusion' ? 'fusiongraph' : 'supergraph'; - - if (cluster.isPrimary) { - let forkNum: number; - if (!process.env.FORK || process.env.FORK === 'true') { - forkNum = process.env.NODE_ENV === 'production' ? availableParallelism() : 1; - } else if ( - process.env.FORK === 'false' || - process.env.FORK === '0' || - process.env.FORK === '1' - ) { - forkNum = 1; - } else if (!isNaN(parseInt(process.env.FORK))) { - forkNum = parseInt(process.env.FORK); - } - - if (typeof unifiedGraphPath === 'string' && !unifiedGraphPath.includes('://')) { - const parcelWatcher$ = import('@parcel/watcher'); - parcelWatcher$ - .catch(() => { - httpHandler.logger.warn( - `If you want to enable hot reloading on ${unifiedGraphPath}, install "@parcel/watcher"`, - ); - }) - .then(parcelWatcher => { - if (parcelWatcher) { - const absoluteUnifiedGraphPath: string = isAbsolute(unifiedGraphPath as string) - ? (unifiedGraphPath as string) - : join(process.cwd(), unifiedGraphPath as string); - const unifiedGraphDir = dirname(absoluteUnifiedGraphPath); - return parcelWatcher - .subscribe(unifiedGraphDir, (err, events) => { - if (err) { - workerLogger.error(err); - return; - } - if (events.some(event => event.path === absoluteUnifiedGraphPath)) { - workerLogger.info(`${unifiedGraphName} changed`); - if (forkNum > 1) { - for (const workerId in cluster.workers) { - cluster.workers[workerId].send('invalidateUnifiedGraph'); - } - } else { - httpHandler.invalidateUnifiedGraph(); - } - } - }) - .then(subscription => { - registerTerminateHandler(eventName => { - workerLogger.info( - `Closing watcher for ${absoluteUnifiedGraphPath} for ${eventName}`, - ); - return subscription.unsubscribe(); - }); - }); - } - return null; - }) - .catch(e => { - workerLogger.error(`Failed to watch ${unifiedGraphPath}`, e); - }); - } - - if (forkNum > 1) { - workerLogger.info(`Forking ${forkNum} ${productName} Workers`); - for (let i = 0; i < forkNum; i++) { - workerLogger.info(`Forking ${productName} Worker #${i}`); - const worker = cluster.fork(); - registerTerminateHandler(eventName => { - workerLogger.info(`Closing ${productName} Worker #${i} for ${eventName}`); - worker.kill(eventName); - workerLogger.info(`Closed ${productName} Worker #${i} for ${eventName}`); - }); - workerLogger.info(`Forked ${productName} Worker #${i}`); - } - workerLogger.info(`Forked ${forkNum} ${productName} Workers`); - - return; - } - } - - const port = meshServeCLIConfig.port || 4000; - const host = - meshServeCLIConfig.host || - platform() === 'win32' || - // is WSL? - release().toLowerCase().includes('microsoft') - ? '127.0.0.1' - : '0.0.0.0'; - const httpHandler = createServeRuntime({ - logging: workerLogger, - ...meshServeCLIConfig, - }); - process.on('message', message => { - if (message === 'invalidateUnifiedGraph') { - workerLogger.info(`Invalidating ${unifiedGraphName}`); - httpHandler.invalidateUnifiedGraph(); - } - }); - const app = meshServeCLIConfig.sslCredentials ? SSLApp(meshServeCLIConfig.sslCredentials) : App(); - const protocol = meshServeCLIConfig.sslCredentials ? 'https' : 'http'; - app.any('/*', httpHandler); - workerLogger.info(`Starting server on ${protocol}://${host}:${port}`); - app.listen(host, port, function listenCallback(listenSocket) { - if (listenSocket) { - workerLogger.info(`Started server on ${protocol}://${host}:${port}`); - registerTerminateHandler(eventName => { - workerLogger.info(`Closing ${protocol}://${host}:${port} for ${eventName}`); - app.close(); - }); - } else { - workerLogger.error(`Failed to start server on ${protocol}://${host}:${port}`); - processExit(1); - } - }); -} diff --git a/yarn.lock b/yarn.lock index a43bc65295258..ee9c2658e08df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3272,6 +3272,15 @@ __metadata: languageName: node linkType: hard +"@commander-js/extra-typings@npm:^12.0.1": + version: 12.0.1 + resolution: "@commander-js/extra-typings@npm:12.0.1" + peerDependencies: + commander: ~12.0.0 + checksum: 10c0/cc3219c70df3ab5c9d3a1f34eda7bd982762b5f68add49067aa80a7ae56a42211c46984e638263bd598c50d2714ed2dbab71301e823970a6bcc14b0303c39d81 + languageName: node + linkType: hard + "@contrast/fn-inspect@npm:^3.3.0": version: 3.4.0 resolution: "@contrast/fn-inspect@npm:3.4.0" @@ -3575,9 +3584,9 @@ __metadata: languageName: unknown linkType: soft -"@e2e/compose-to-target@workspace:e2e/compose-to-target": +"@e2e/compose-to-output@workspace:e2e/compose-to-output": version: 0.0.0-use.local - resolution: "@e2e/compose-to-target@workspace:e2e/compose-to-target" + resolution: "@e2e/compose-to-output@workspace:e2e/compose-to-output" dependencies: "@graphql-mesh/compose-cli": "workspace:*" "@graphql-mesh/serve-cli": "workspace:*" @@ -4690,15 +4699,15 @@ __metadata: version: 0.0.0-use.local resolution: "@graphql-mesh/compose-cli@workspace:packages/compose-cli" dependencies: + "@commander-js/extra-typings": "npm:^12.0.1" "@graphql-mesh/fusion-composition": "npm:^0.0.2" "@graphql-mesh/utils": "npm:^0.97.5" "@graphql-tools/graphql-file-loader": "npm:8.0.1" "@graphql-tools/load": "npm:^8.0.1" "@graphql-tools/utils": "npm:^10.0.8" - "@types/spinnies": "npm:^0.5.3" "@whatwg-node/fetch": "npm:^0.9.14" + commander: "npm:^12.0.0" dotenv: "npm:^16.3.1" - spinnies: "npm:^0.5.1" tsx: "npm:^4.7.1" peerDependencies: "@graphql-mesh/types": ^0.97.5 @@ -5355,12 +5364,15 @@ __metadata: version: 0.0.0-use.local resolution: "@graphql-mesh/serve-cli@workspace:packages/serve-cli" dependencies: + "@commander-js/extra-typings": "npm:^12.0.1" "@graphql-mesh/cross-helpers": "npm:^0.4.1" "@graphql-mesh/serve-runtime": "npm:^0.2.12" "@graphql-mesh/types": "npm:^0.97.5" "@graphql-mesh/utils": "npm:^0.97.5" + "@graphql-tools/utils": "npm:^10.1.3" "@parcel/watcher": "npm:^2.3.0" "@types/spinnies": "npm:^0.5.3" + commander: "npm:^12.0.0" dotenv: "npm:^16.3.1" json-bigint-patch: "npm:^0.0.8" node-libcurl: "npm:^4.0.0"