nitro-graphql 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -461,10 +461,11 @@ export default defineGraphQLConfig({
461
461
 
462
462
  Try out the examples:
463
463
 
464
- - **Standalone Nitro**: [`playground/`](playground/)
465
- - **Nuxt.js Integration**: [`playground-nuxt/`](playground-nuxt/)
464
+ - **Standalone Nitro**: [`playgrounds/nitro/`](playgrounds/nitro/)
465
+ - **Nuxt.js Integration**: [`playgrounds/nuxt/`](playgrounds/nuxt/)
466
+ - **Apollo Federation**: [`playgrounds/federation/`](playgrounds/federation/)
466
467
 
467
- Both examples include working GraphQL schemas, resolvers, and demonstrate the module's capabilities.
468
+ All examples include working GraphQL schemas, resolvers, and demonstrate the module's capabilities.
468
469
 
469
470
  ## 🔧 API Reference
470
471
 
@@ -1203,7 +1204,9 @@ const client = createGraphQLClient({
1203
1204
  - `pnpm build` - Build the module
1204
1205
  - `pnpm dev` - Watch mode with automatic rebuilding
1205
1206
  - `pnpm lint` - ESLint with auto-fix
1206
- - `pnpm playground` - Run the Nitro playground example
1207
+ - `pnpm playground:nitro` - Run the Nitro playground example
1208
+ - `pnpm playground:nuxt` - Run the Nuxt playground example
1209
+ - `pnpm playground:federation` - Run the Apollo Federation playground
1207
1210
  - `pnpm release` - Build, version bump, and publish
1208
1211
 
1209
1212
  ### Requirements
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "pathe";
3
- import { defineNuxtModule } from "@nuxt/kit";
3
+ import { defineNuxtModule, getLayerDirectories } from "@nuxt/kit";
4
4
 
5
5
  //#region src/ecosystem/nuxt.ts
6
6
  var nuxt_default = defineNuxtModule({
@@ -30,10 +30,19 @@ var nuxt_default = defineNuxtModule({
30
30
  const externalServices = nuxt.options.nitro?.graphql?.externalServices || [];
31
31
  for (const service of externalServices) nuxt.options.alias[`#graphql/client/${service.name}`] = join(nuxt.options.buildDir, `types/nitro-graphql-client-${service.name}.d.ts`);
32
32
  nuxt.hook("imports:dirs", (dirs) => {
33
- dirs.push(resolve(nuxt.options.srcDir, "graphql"));
33
+ const graphqlServerPath = nuxt.options.nitro?.graphql?.serverDir || resolve(nuxt.options.srcDir, "graphql");
34
+ dirs.push(graphqlServerPath);
34
35
  });
35
- nuxt.hook("nitro:config", () => {
36
+ nuxt.hook("nitro:config", async (nitroConfig) => {
36
37
  const clientDir = join(nuxt.options.buildDir, "graphql");
38
+ const layerDirs = await getLayerDirectories(nuxt);
39
+ const layerDirectories = layerDirs.map((layer) => layer.root.replace(/\/$/, "")).filter((root) => root !== nuxt.options.rootDir);
40
+ const layerServerDirs = layerDirs.filter((layer) => layer.root !== `${nuxt.options.rootDir}/`).map((layer) => layer.server?.replace(/\/$/, "")).filter(Boolean);
41
+ const layerAppDirs = layerDirs.filter((layer) => layer.root !== `${nuxt.options.rootDir}/`).map((layer) => layer.app?.replace(/\/$/, "")).filter(Boolean);
42
+ if (!nitroConfig.graphql) nitroConfig.graphql = {};
43
+ nitroConfig.graphql.layerDirectories = layerDirectories;
44
+ nitroConfig.graphql.layerServerDirs = layerServerDirs;
45
+ nitroConfig.graphql.layerAppDirs = layerAppDirs;
37
46
  const appGraphqlDir = resolve(nuxt.options.rootDir, "app/graphql");
38
47
  const hasAppGraphqlDir = existsSync(appGraphqlDir);
39
48
  if (!hasAppGraphqlDir) {
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { StandardSchemaV1 } from "./types/standard-schema.js";
2
- import { CodegenClientConfig, CodegenServerConfig, ExternalGraphQLService, GenImport, GenericSdkConfig, NitroGraphQLOptions } from "./types/index.js";
2
+ import { CodegenClientConfig, CodegenServerConfig, ExternalGraphQLService, FederationConfig, GenImport, GenericSdkConfig, NitroGraphQLOptions } from "./types/index.js";
3
3
  import * as nitropack0 from "nitropack";
4
4
 
5
5
  //#region src/index.d.ts
6
6
  declare const _default: nitropack0.NitroModule;
7
7
  //#endregion
8
- export { CodegenClientConfig, CodegenServerConfig, ExternalGraphQLService, GenImport, GenericSdkConfig, NitroGraphQLOptions, StandardSchemaV1, _default as default };
8
+ export { CodegenClientConfig, CodegenServerConfig, ExternalGraphQLService, FederationConfig, GenImport, GenericSdkConfig, NitroGraphQLOptions, StandardSchemaV1, _default as default };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { generateDirectiveSchemas } from "./utils/directive-parser.js";
2
- import { relativeWithDot, scanDirectives, scanDocs, scanResolvers, scanSchemas, validateExternalServices } from "./utils/index.js";
2
+ import { generateLayerIgnorePatterns, getLayerAppDirectories, getLayerServerDirectories, relativeWithDot, scanDirectives, scanDocs, scanResolvers, scanSchemas, validateExternalServices } from "./utils/index.js";
3
3
  import { clientTypeGeneration, serverTypeGeneration } from "./utils/type-generation.js";
4
4
  import { rollupConfig } from "./rollup.js";
5
5
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
@@ -35,7 +35,7 @@ var src_default = defineNitroModule({
35
35
  server: "server"
36
36
  }
37
37
  };
38
- nitro.hooks.hook("rollup:before", (nitro$1, rollupConfig$1) => {
38
+ nitro.hooks.hook("rollup:before", (_, rollupConfig$1) => {
39
39
  rollupConfig$1.external = rollupConfig$1.external || [];
40
40
  const codegenExternals = ["oxc-parser", "@oxc-parser"];
41
41
  if (Array.isArray(rollupConfig$1.external)) rollupConfig$1.external.push(...codegenExternals);
@@ -54,15 +54,21 @@ var src_default = defineNitroModule({
54
54
  },
55
55
  playground: true
56
56
  });
57
+ if (nitro.options.graphql?.federation?.enabled) consola.info(`Apollo Federation enabled for service: ${nitro.options.graphql.federation.serviceName || "unnamed"}`);
57
58
  const graphqlBuildDir = resolve(nitro.options.buildDir, "graphql");
58
59
  nitro.graphql.buildDir = graphqlBuildDir;
59
60
  const watchDirs = [];
60
61
  switch (nitro.options.framework.name) {
61
- case "nuxt":
62
+ case "nuxt": {
62
63
  watchDirs.push(join(nitro.options.rootDir, "app", "graphql"));
63
64
  nitro.graphql.clientDir = resolve(nitro.options.rootDir, "app", "graphql");
64
65
  nitro.graphql.dir.client = "app/graphql";
66
+ const layerServerDirs = getLayerServerDirectories(nitro);
67
+ const layerAppDirs = getLayerAppDirectories(nitro);
68
+ for (const layerServerDir of layerServerDirs) watchDirs.push(join(layerServerDir, "graphql"));
69
+ for (const layerAppDir of layerAppDirs) watchDirs.push(join(layerAppDir, "graphql"));
65
70
  break;
71
+ }
66
72
  case "nitro":
67
73
  nitro.graphql.clientDir = resolve(nitro.options.rootDir, "graphql");
68
74
  nitro.graphql.dir.client = "graphql";
@@ -80,8 +86,8 @@ var src_default = defineNitroModule({
80
86
  const watcher = watch(watchDirs, {
81
87
  persistent: true,
82
88
  ignoreInitial: true,
83
- ignored: [...nitro.options.ignore, "**/server/graphql/_directives.graphql"]
84
- }).on("all", async (event, path) => {
89
+ ignored: [...nitro.options.ignore, ...generateLayerIgnorePatterns(nitro)]
90
+ }).on("all", async (_, path) => {
85
91
  if (path.endsWith(".graphql") || path.endsWith(".gql")) await clientTypeGeneration(nitro);
86
92
  });
87
93
  nitro.hooks.hook("close", () => {
@@ -154,7 +160,7 @@ var src_default = defineNitroModule({
154
160
  ]
155
161
  });
156
162
  }
157
- nitro.hooks.hook("rollup:before", (nitro$1, rollupConfig$1) => {
163
+ nitro.hooks.hook("rollup:before", (_, rollupConfig$1) => {
158
164
  const manualChunks = rollupConfig$1.output?.manualChunks;
159
165
  const chunkFiles = rollupConfig$1.output?.chunkFileNames;
160
166
  if (!rollupConfig$1.output.inlineDynamicImports) rollupConfig$1.output.manualChunks = (id, meta) => {
package/dist/rollup.js CHANGED
@@ -11,6 +11,7 @@ async function rollupConfig(app) {
11
11
  virtualResolvers(app);
12
12
  virtualDirectives(app);
13
13
  getGraphQLConfig(app);
14
+ virtualModuleConfig(app);
14
15
  app.hooks.hook("rollup:before", (nitro, rollupConfig$1) => {
15
16
  rollupConfig$1.plugins = rollupConfig$1.plugins || [];
16
17
  const { include = /\.(graphql|gql)$/i, exclude, validate = false } = app.options.graphql?.loader || {};
@@ -25,8 +26,9 @@ async function rollupConfig(app) {
25
26
  if (validate) parse(content);
26
27
  return `export default ${JSON.stringify(content)}`;
27
28
  } catch (error) {
28
- if (error.code === "ENOENT") return null;
29
- this.error(`Failed to read GraphQL file ${id}: ${error.message}`);
29
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return null;
30
+ const message = error instanceof Error ? error.message : String(error);
31
+ this.error(`Failed to read GraphQL file ${id}: ${message}`);
30
32
  }
31
33
  }
32
34
  });
@@ -35,7 +37,7 @@ async function rollupConfig(app) {
35
37
  async buildStart() {
36
38
  const graphqlFiles = await scanGraphql(nitro);
37
39
  for (const file of graphqlFiles) this.addWatchFile(file);
38
- this.addWatchFile("server/graphql/");
40
+ this.addWatchFile(app.graphql.serverDir);
39
41
  }
40
42
  });
41
43
  }
@@ -119,6 +121,13 @@ export { importedConfig }
119
121
  `;
120
122
  };
121
123
  }
124
+ function virtualModuleConfig(app) {
125
+ app.options.virtual ??= {};
126
+ app.options.virtual["#nitro-internal-virtual/module-config"] = () => {
127
+ const moduleConfig = app.options.graphql || {};
128
+ return `export const moduleConfig = ${JSON.stringify(moduleConfig, null, 2)};`;
129
+ };
130
+ }
122
131
 
123
132
  //#endregion
124
133
  export { rollupConfig };
@@ -1,6 +1,6 @@
1
1
  import * as h30 from "h3";
2
2
 
3
3
  //#region src/routes/apollo-server.d.ts
4
- declare const _default: h30.EventHandler<h30.EventHandlerRequest, any>;
4
+ declare const _default: h30.EventHandler<h30.EventHandlerRequest, Promise<any>>;
5
5
  //#endregion
6
6
  export { _default as default };
@@ -1,16 +1,31 @@
1
1
  import { startServerAndCreateH3Handler } from "../utils/apollo.js";
2
+ import { consola as consola$1 } from "consola";
2
3
  import defu from "defu";
4
+ import { parse } from "graphql";
3
5
  import { mergeResolvers, mergeTypeDefs } from "@graphql-tools/merge";
4
6
  import { importedConfig } from "#nitro-internal-virtual/graphql-config";
7
+ import { moduleConfig } from "#nitro-internal-virtual/module-config";
5
8
  import { directives } from "#nitro-internal-virtual/server-directives";
6
9
  import { resolvers } from "#nitro-internal-virtual/server-resolvers";
7
10
  import { schemas } from "#nitro-internal-virtual/server-schemas";
8
11
  import { ApolloServer } from "@apollo/server";
9
12
  import { ApolloServerPluginLandingPageLocalDefault } from "@apollo/server/plugin/landingPage/default";
10
13
  import { makeExecutableSchema } from "@graphql-tools/schema";
14
+ import { defineEventHandler } from "h3";
11
15
 
12
16
  //#region src/routes/apollo-server.ts
13
- function createMergedSchema() {
17
+ let buildSubgraphSchema = null;
18
+ async function loadFederationSupport() {
19
+ if (buildSubgraphSchema !== null) return buildSubgraphSchema;
20
+ try {
21
+ const apolloSubgraph = await import("@apollo/subgraph");
22
+ buildSubgraphSchema = apolloSubgraph.buildSubgraphSchema;
23
+ } catch {
24
+ buildSubgraphSchema = false;
25
+ }
26
+ return buildSubgraphSchema;
27
+ }
28
+ async function createMergedSchema() {
14
29
  try {
15
30
  const mergedSchemas = schemas.map((schema$1) => schema$1.def).join("\n\n");
16
31
  const typeDefs = mergeTypeDefs([mergedSchemas], {
@@ -19,7 +34,24 @@ function createMergedSchema() {
19
34
  sort: true
20
35
  });
21
36
  const mergedResolvers = mergeResolvers(resolvers.map((r) => r.resolver));
22
- let schema = makeExecutableSchema({
37
+ const federationEnabled = moduleConfig.federation?.enabled;
38
+ let schema;
39
+ if (federationEnabled) {
40
+ const buildSubgraph = await loadFederationSupport();
41
+ if (buildSubgraph) {
42
+ const typeDefsDoc = typeof typeDefs === "string" ? parse(typeDefs) : typeDefs;
43
+ schema = buildSubgraph({
44
+ typeDefs: typeDefsDoc,
45
+ resolvers: mergedResolvers
46
+ });
47
+ } else {
48
+ console.warn("Federation enabled but @apollo/subgraph not available, falling back to regular schema");
49
+ schema = makeExecutableSchema({
50
+ typeDefs,
51
+ resolvers: mergedResolvers
52
+ });
53
+ }
54
+ } else schema = makeExecutableSchema({
23
55
  typeDefs,
24
56
  resolvers: mergedResolvers
25
57
  });
@@ -28,14 +60,14 @@ function createMergedSchema() {
28
60
  }
29
61
  return schema;
30
62
  } catch (error) {
31
- console.error("Schema merge error:", error);
63
+ consola$1.error("Schema merge error:", error);
32
64
  throw error;
33
65
  }
34
66
  }
35
67
  let apolloServer = null;
36
- function createApolloServer() {
68
+ async function createApolloServer() {
37
69
  if (!apolloServer) {
38
- const schema = createMergedSchema();
70
+ const schema = await createMergedSchema();
39
71
  apolloServer = new ApolloServer(defu({
40
72
  schema,
41
73
  introspection: true,
@@ -44,7 +76,13 @@ function createApolloServer() {
44
76
  }
45
77
  return apolloServer;
46
78
  }
47
- var apollo_server_default = startServerAndCreateH3Handler(apolloServer || createApolloServer, { context: async (event) => ({ event }) });
79
+ let serverPromise = null;
80
+ var apollo_server_default = defineEventHandler(async (event) => {
81
+ if (!serverPromise) serverPromise = createApolloServer();
82
+ const server = await serverPromise;
83
+ const h3Handler = startServerAndCreateH3Handler(server, { context: async () => ({ event }) });
84
+ return h3Handler(event);
85
+ });
48
86
 
49
87
  //#endregion
50
88
  export { apollo_server_default as default };
@@ -1,6 +1,9 @@
1
+ import { consola as consola$1 } from "consola";
1
2
  import defu from "defu";
3
+ import { parse } from "graphql";
2
4
  import { mergeResolvers, mergeTypeDefs } from "@graphql-tools/merge";
3
5
  import { importedConfig } from "#nitro-internal-virtual/graphql-config";
6
+ import { moduleConfig } from "#nitro-internal-virtual/module-config";
4
7
  import { directives } from "#nitro-internal-virtual/server-directives";
5
8
  import { resolvers } from "#nitro-internal-virtual/server-resolvers";
6
9
  import { schemas } from "#nitro-internal-virtual/server-schemas";
@@ -9,6 +12,17 @@ import { defineEventHandler, toWebRequest } from "h3";
9
12
  import { createYoga } from "graphql-yoga";
10
13
 
11
14
  //#region src/routes/graphql-yoga.ts
15
+ let buildSubgraphSchema = null;
16
+ async function loadFederationSupport() {
17
+ if (buildSubgraphSchema !== null) return buildSubgraphSchema;
18
+ try {
19
+ const apolloSubgraph = await import("@apollo/subgraph");
20
+ buildSubgraphSchema = apolloSubgraph.buildSubgraphSchema;
21
+ } catch {
22
+ buildSubgraphSchema = false;
23
+ }
24
+ return buildSubgraphSchema;
25
+ }
12
26
  const apolloSandboxHtml = `<!DOCTYPE html>
13
27
  <html lang="en">
14
28
  <body style="margin: 0; overflow-x: hidden; overflow-y: hidden">
@@ -26,7 +40,7 @@ new window.EmbeddedSandbox({
26
40
  <\/script>
27
41
  </body>
28
42
  </html>`;
29
- function createMergedSchema() {
43
+ async function createMergedSchema() {
30
44
  try {
31
45
  const mergedSchemas = schemas.map((schema$1) => schema$1.def).join("\n\n");
32
46
  const typeDefs = mergeTypeDefs([mergedSchemas], {
@@ -35,7 +49,24 @@ function createMergedSchema() {
35
49
  sort: true
36
50
  });
37
51
  const mergedResolvers = mergeResolvers(resolvers.map((r) => r.resolver));
38
- let schema = makeExecutableSchema({
52
+ const federationEnabled = moduleConfig.federation?.enabled;
53
+ let schema;
54
+ if (federationEnabled) {
55
+ const buildSubgraph = await loadFederationSupport();
56
+ if (buildSubgraph) {
57
+ const typeDefsDoc = typeof typeDefs === "string" ? parse(typeDefs) : typeDefs;
58
+ schema = buildSubgraph({
59
+ typeDefs: typeDefsDoc,
60
+ resolvers: mergedResolvers
61
+ });
62
+ } else {
63
+ console.warn("Federation enabled but @apollo/subgraph not available, falling back to regular schema");
64
+ schema = makeExecutableSchema({
65
+ typeDefs,
66
+ resolvers: mergedResolvers
67
+ });
68
+ }
69
+ } else schema = makeExecutableSchema({
39
70
  typeDefs,
40
71
  resolvers: mergedResolvers
41
72
  });
@@ -44,14 +75,14 @@ function createMergedSchema() {
44
75
  }
45
76
  return schema;
46
77
  } catch (error) {
47
- console.error("Schema merge error:", error);
78
+ consola$1.error("Schema merge error:", error);
48
79
  throw error;
49
80
  }
50
81
  }
51
82
  let yoga;
52
83
  var graphql_yoga_default = defineEventHandler(async (event) => {
53
84
  if (!yoga) {
54
- const schema = createMergedSchema();
85
+ const schema = await createMergedSchema();
55
86
  yoga = createYoga(defu({
56
87
  schema,
57
88
  graphqlEndpoint: "/api/graphql",
@@ -84,6 +84,16 @@ interface ExternalGraphQLService {
84
84
  clientSDK?: GenericSdkConfig;
85
85
  };
86
86
  }
87
+ interface FederationConfig {
88
+ /** Enable Apollo Federation subgraph support */
89
+ enabled: boolean;
90
+ /** Service name for federation (used in subgraph config) */
91
+ serviceName?: string;
92
+ /** Service version for federation */
93
+ serviceVersion?: string;
94
+ /** Service URL for federation gateway */
95
+ serviceUrl?: string;
96
+ }
87
97
  interface NitroGraphQLOptions {
88
98
  framework: 'graphql-yoga' | 'apollo-server';
89
99
  endpoint?: {
@@ -105,6 +115,14 @@ interface NitroGraphQLOptions {
105
115
  };
106
116
  /** External GraphQL services to generate types and SDKs for */
107
117
  externalServices?: ExternalGraphQLService[];
118
+ /** Apollo Federation configuration */
119
+ federation?: FederationConfig;
120
+ /** Server GraphQL directory path */
121
+ serverDir?: string;
122
+ /** Layer directories (populated by Nuxt module) */
123
+ layerDirectories?: string[];
124
+ layerServerDirs?: string[];
125
+ layerAppDirs?: string[];
108
126
  }
109
127
  //#endregion
110
- export { CodegenClientConfig, CodegenServerConfig, ExternalGraphQLService, GenImport, GenericSdkConfig, NitroGraphQLOptions };
128
+ export { CodegenClientConfig, CodegenServerConfig, ExternalGraphQLService, FederationConfig, GenImport, GenericSdkConfig, NitroGraphQLOptions };
@@ -4,6 +4,22 @@ import { Nitro } from "nitropack";
4
4
 
5
5
  //#region src/utils/index.d.ts
6
6
  declare const GLOB_SCAN_PATTERN = "**/*.{graphql,gql,js,mjs,cjs,ts,mts,cts,tsx,jsx}";
7
+ /**
8
+ * Get all Nuxt layer directories from Nitro config
9
+ */
10
+ declare function getLayerDirectories(nitro: Nitro): string[];
11
+ /**
12
+ * Get all Nuxt layer server directories from Nitro config
13
+ */
14
+ declare function getLayerServerDirectories(nitro: Nitro): string[];
15
+ /**
16
+ * Get all Nuxt layer app directories from Nitro config
17
+ */
18
+ declare function getLayerAppDirectories(nitro: Nitro): string[];
19
+ /**
20
+ * Generate layer-aware ignore patterns for auto-generated files
21
+ */
22
+ declare function generateLayerIgnorePatterns(nitro: Nitro): string[];
7
23
  declare function getImportId(p: string, lazy?: boolean): string;
8
24
  declare function relativeWithDot(from: string, to: string): string;
9
25
  declare function scanGraphql(nitro: Nitro): Promise<string[]>;
@@ -19,6 +35,6 @@ declare function scanExternalServiceDocs(nitro: Nitro, serviceName: string, patt
19
35
  /**
20
36
  * Validate external GraphQL service configuration
21
37
  */
22
- declare function validateExternalServices(services: any[]): string[];
38
+ declare function validateExternalServices(services: unknown[]): string[];
23
39
  //#endregion
24
- export { GLOB_SCAN_PATTERN, directiveParser, generateDirectiveSchema, generateDirectiveSchemas, getImportId, relativeWithDot, scanDirectives, scanDocs, scanExternalServiceDocs, scanGraphql, scanResolvers, scanSchemas, scanTypeDefs, validateExternalServices };
40
+ export { GLOB_SCAN_PATTERN, directiveParser, generateDirectiveSchema, generateDirectiveSchemas, generateLayerIgnorePatterns, getImportId, getLayerAppDirectories, getLayerDirectories, getLayerServerDirectories, relativeWithDot, scanDirectives, scanDocs, scanExternalServiceDocs, scanGraphql, scanResolvers, scanSchemas, scanTypeDefs, validateExternalServices };
@@ -7,6 +7,34 @@ import { glob } from "tinyglobby";
7
7
 
8
8
  //#region src/utils/index.ts
9
9
  const GLOB_SCAN_PATTERN = "**/*.{graphql,gql,js,mjs,cjs,ts,mts,cts,tsx,jsx}";
10
+ /**
11
+ * Get all Nuxt layer directories from Nitro config
12
+ */
13
+ function getLayerDirectories(nitro) {
14
+ return nitro.options.graphql?.layerDirectories || [];
15
+ }
16
+ /**
17
+ * Get all Nuxt layer server directories from Nitro config
18
+ */
19
+ function getLayerServerDirectories(nitro) {
20
+ return nitro.options.graphql?.layerServerDirs || [];
21
+ }
22
+ /**
23
+ * Get all Nuxt layer app directories from Nitro config
24
+ */
25
+ function getLayerAppDirectories(nitro) {
26
+ return nitro.options.graphql?.layerAppDirs || [];
27
+ }
28
+ /**
29
+ * Generate layer-aware ignore patterns for auto-generated files
30
+ */
31
+ function generateLayerIgnorePatterns(nitro) {
32
+ const patterns = [];
33
+ patterns.push(`${nitro.graphql.serverDir}/_directives.graphql`);
34
+ const layerServerDirs = nitro.options.graphql?.layerServerDirs || [];
35
+ for (const layerServerDir of layerServerDirs) patterns.push(`${layerServerDir}/graphql/_directives.graphql`);
36
+ return patterns;
37
+ }
10
38
  function getImportId(p, lazy) {
11
39
  return (lazy ? "_lazy_" : "_") + hash(p).replace(/-/g, "").slice(0, 6);
12
40
  }
@@ -35,32 +63,32 @@ async function scanResolvers(nitro) {
35
63
  if (decl.init.callee.type === "Identifier" && decl.init.callee.name === "defineResolver") exports.imports.push({
36
64
  name: decl.id.name,
37
65
  type: "resolver",
38
- as: `_${hash(decl.id.name + file.path).replace(/-/g, "").slice(0, 6)}`
66
+ as: `_${hash(decl.id.name + file.fullPath).replace(/-/g, "").slice(0, 6)}`
39
67
  });
40
68
  if (decl.init.callee.type === "Identifier" && decl.init.callee.name === "defineQuery") exports.imports.push({
41
69
  name: decl.id.name,
42
70
  type: "query",
43
- as: `_${hash(decl.id.name + file.path).replace(/-/g, "").slice(0, 6)}`
71
+ as: `_${hash(decl.id.name + file.fullPath).replace(/-/g, "").slice(0, 6)}`
44
72
  });
45
73
  if (decl.init.callee.type === "Identifier" && decl.init.callee.name === "defineMutation") exports.imports.push({
46
74
  name: decl.id.name,
47
75
  type: "mutation",
48
- as: `_${hash(decl.id.name + file.path).replace(/-/g, "").slice(0, 6)}`
76
+ as: `_${hash(decl.id.name + file.fullPath).replace(/-/g, "").slice(0, 6)}`
49
77
  });
50
78
  if (decl.init.callee.type === "Identifier" && decl.init.callee.name === "defineType") exports.imports.push({
51
79
  name: decl.id.name,
52
80
  type: "type",
53
- as: `_${hash(decl.id.name + file.path).replace(/-/g, "").slice(0, 6)}`
81
+ as: `_${hash(decl.id.name + file.fullPath).replace(/-/g, "").slice(0, 6)}`
54
82
  });
55
83
  if (decl.init.callee.type === "Identifier" && decl.init.callee.name === "defineSubscription") exports.imports.push({
56
84
  name: decl.id.name,
57
85
  type: "subscription",
58
- as: `_${hash(decl.id.name + file.path).replace(/-/g, "").slice(0, 6)}`
86
+ as: `_${hash(decl.id.name + file.fullPath).replace(/-/g, "").slice(0, 6)}`
59
87
  });
60
88
  if (decl.init.callee.type === "Identifier" && decl.init.callee.name === "defineDirective") exports.imports.push({
61
89
  name: decl.id.name,
62
90
  type: "directive",
63
- as: `_${hash(decl.id.name + file.path).replace(/-/g, "").slice(0, 6)}`
91
+ as: `_${hash(decl.id.name + file.fullPath).replace(/-/g, "").slice(0, 6)}`
64
92
  });
65
93
  }
66
94
  }
@@ -85,7 +113,7 @@ async function scanDirectives(nitro) {
85
113
  if (decl.init.callee.type === "Identifier" && decl.init.callee.name === "defineDirective") exports.imports.push({
86
114
  name: decl.id.name,
87
115
  type: "directive",
88
- as: `_${hash(decl.id.name + file.path).replace(/-/g, "").slice(0, 6)}`
116
+ as: `_${hash(decl.id.name + file.fullPath).replace(/-/g, "").slice(0, 6)}`
89
117
  });
90
118
  }
91
119
  }
@@ -104,12 +132,22 @@ async function scanSchemas(nitro) {
104
132
  }
105
133
  async function scanDocs(nitro) {
106
134
  const files = await scanDir(nitro, nitro.options.rootDir, nitro.graphql.dir.client, "**/*.graphql");
135
+ const layerAppDirs = getLayerAppDirectories(nitro);
136
+ const layerFiles = await Promise.all(layerAppDirs.map((layerAppDir) => scanDir(nitro, layerAppDir, "graphql", "**/*.graphql"))).then((r) => r.flat());
137
+ const combinedFiles = [...files, ...layerFiles];
138
+ const seenPaths = /* @__PURE__ */ new Set();
139
+ const allFiles = combinedFiles.filter((file) => {
140
+ if (seenPaths.has(file.fullPath)) return false;
141
+ seenPaths.add(file.fullPath);
142
+ return true;
143
+ });
107
144
  const externalServices = nitro.options.graphql?.externalServices || [];
108
145
  const externalPatterns = externalServices.flatMap((service) => service.documents || []);
109
- return files.filter((f) => !f.path.startsWith("external/")).filter((f) => {
146
+ return allFiles.filter((f) => !f.path.startsWith("external/")).filter((f) => {
110
147
  const relativePath = f.path;
111
148
  for (const pattern of externalPatterns) {
112
- const cleanPattern = pattern.replace(/^app\/graphql\//, "");
149
+ const clientDirPattern = `${nitro.graphql.dir.client}/`;
150
+ const cleanPattern = pattern.replace(/* @__PURE__ */ new RegExp(`^${clientDirPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`), "");
113
151
  const patternDir = cleanPattern.split("/")[0];
114
152
  const fileDir = relativePath.split("/")[0];
115
153
  if (patternDir === fileDir) return false;
@@ -144,23 +182,35 @@ function validateExternalServices(services) {
144
182
  const serviceNames = /* @__PURE__ */ new Set();
145
183
  for (const [index, service] of services.entries()) {
146
184
  const prefix = `externalServices[${index}]`;
147
- if (!service.name || typeof service.name !== "string") errors.push(`${prefix}.name is required and must be a string`);
185
+ if (!service || typeof service !== "object") {
186
+ errors.push(`${prefix} must be an object`);
187
+ continue;
188
+ }
189
+ if (!("name" in service) || typeof service.name !== "string") errors.push(`${prefix}.name is required and must be a string`);
148
190
  else if (serviceNames.has(service.name)) errors.push(`${prefix}.name "${service.name}" must be unique`);
149
191
  else serviceNames.add(service.name);
150
- if (!service.schema) errors.push(`${prefix}.schema is required`);
151
- if (!service.endpoint || typeof service.endpoint !== "string") errors.push(`${prefix}.endpoint is required and must be a string`);
192
+ if (!("schema" in service) || !service.schema) errors.push(`${prefix}.schema is required`);
193
+ if (!("endpoint" in service) || typeof service.endpoint !== "string") errors.push(`${prefix}.endpoint is required and must be a string`);
152
194
  else try {
153
195
  new URL(service.endpoint);
154
196
  } catch {
155
197
  errors.push(`${prefix}.endpoint "${service.endpoint}" must be a valid URL`);
156
198
  }
157
- if (service.name && !/^[a-z]\w*$/i.test(service.name)) errors.push(`${prefix}.name "${service.name}" must be a valid identifier (letters, numbers, underscore, starting with letter)`);
199
+ if ("name" in service && service.name && typeof service.name === "string" && !/^[a-z]\w*$/i.test(service.name)) errors.push(`${prefix}.name "${service.name}" must be a valid identifier (letters, numbers, underscore, starting with letter)`);
158
200
  }
159
201
  return errors;
160
202
  }
161
203
  async function scanFiles(nitro, name, globPattern = GLOB_SCAN_PATTERN) {
162
- const files = await Promise.all(nitro.options.scanDirs.map((dir) => scanDir(nitro, dir, name, globPattern))).then((r) => r.flat());
163
- return files;
204
+ const regularFiles = await Promise.all(nitro.options.scanDirs.map((dir) => scanDir(nitro, dir, name, globPattern))).then((r) => r.flat());
205
+ const layerDirectories = getLayerDirectories(nitro);
206
+ const layerFiles = await Promise.all(layerDirectories.map((layerDir) => scanDir(nitro, layerDir, `server/${name}`, globPattern))).then((r) => r.flat());
207
+ const allFiles = [...regularFiles, ...layerFiles];
208
+ const seenPaths = /* @__PURE__ */ new Set();
209
+ return allFiles.filter((file) => {
210
+ if (seenPaths.has(file.fullPath)) return false;
211
+ seenPaths.add(file.fullPath);
212
+ return true;
213
+ });
164
214
  }
165
215
  async function scanDir(nitro, dir, name, globPattern = GLOB_SCAN_PATTERN) {
166
216
  const fileNames = await glob(join(name, globPattern), {
@@ -184,4 +234,4 @@ async function scanDir(nitro, dir, name, globPattern = GLOB_SCAN_PATTERN) {
184
234
  }
185
235
 
186
236
  //#endregion
187
- export { GLOB_SCAN_PATTERN, directiveParser, generateDirectiveSchema, generateDirectiveSchemas, getImportId, relativeWithDot, scanDirectives, scanDocs, scanExternalServiceDocs, scanGraphql, scanResolvers, scanSchemas, scanTypeDefs, validateExternalServices };
237
+ export { GLOB_SCAN_PATTERN, directiveParser, generateDirectiveSchema, generateDirectiveSchemas, generateLayerIgnorePatterns, getImportId, getLayerAppDirectories, getLayerDirectories, getLayerServerDirectories, relativeWithDot, scanDirectives, scanDocs, scanExternalServiceDocs, scanGraphql, scanResolvers, scanSchemas, scanTypeDefs, validateExternalServices };
@@ -1,7 +1,7 @@
1
- import { CodegenServerConfig } from "../types/index.js";
1
+ import { NitroGraphQLOptions } from "../types/index.js";
2
2
  import { GraphQLSchema } from "graphql";
3
3
 
4
4
  //#region src/utils/server-codegen.d.ts
5
- declare function generateTypes(selectFremework: string, schema: GraphQLSchema, config?: CodegenServerConfig, outputPath?: string): Promise<string>;
5
+ declare function generateTypes(selectFremework: string, schema: GraphQLSchema, config?: Partial<NitroGraphQLOptions>, outputPath?: string): Promise<string>;
6
6
  //#endregion
7
7
  export { generateTypes };
@@ -41,9 +41,10 @@ async function generateTypes(selectFremework, schema, config = {}, outputPath) {
41
41
  maybeValue: "T | null | undefined",
42
42
  inputMaybeValue: "T | undefined",
43
43
  declarationKind: "interface",
44
- enumsAsTypes: true
44
+ enumsAsTypes: true,
45
+ ...config.federation?.enabled && { federation: true }
45
46
  };
46
- const mergedConfig = defu$1(defaultConfig, config);
47
+ const mergedConfig = defu$1(defaultConfig, config.codegen?.server);
47
48
  const output = await codegen({
48
49
  filename: outputPath || "types.generated.ts",
49
50
  schema: parse(printSchemaWithDirectives(schema)),
@@ -193,13 +193,36 @@ async function serverTypeGeneration(app) {
193
193
  const isValid = validateNoDuplicateTypes(schemas, schemaStrings);
194
194
  if (!isValid) return;
195
195
  const mergedSchemasString = schemaStrings.join("\n\n");
196
- const mergedSchemas = mergeTypeDefs([mergedSchemasString], {
196
+ const federationEnabled = app.options.graphql?.federation?.enabled === true;
197
+ let schemaWithDirectives = mergedSchemasString;
198
+ if (federationEnabled) {
199
+ const federationDirectives = `
200
+ directive @key(fields: String!) on OBJECT | INTERFACE
201
+ directive @requires(fields: String!) on FIELD_DEFINITION
202
+ directive @provides(fields: String!) on FIELD_DEFINITION
203
+ directive @external on FIELD_DEFINITION | OBJECT
204
+ directive @tag(name: String!) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
205
+ directive @extends on OBJECT | INTERFACE
206
+ directive @shareable on FIELD_DEFINITION | OBJECT
207
+ directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
208
+ directive @override(from: String!) on FIELD_DEFINITION
209
+ directive @composeDirective(name: String!) on SCHEMA
210
+ directive @link(url: String!, as: String, for: Purpose, import: [String!]) on SCHEMA
211
+
212
+ enum Purpose {
213
+ SECURITY
214
+ EXECUTION
215
+ }
216
+ `;
217
+ schemaWithDirectives = `${federationDirectives}\n\n${mergedSchemasString}`;
218
+ }
219
+ const mergedSchemas = mergeTypeDefs([schemaWithDirectives], {
197
220
  throwOnConflict: true,
198
221
  commentDescriptions: true,
199
222
  sort: true
200
223
  });
201
224
  const schema = buildSchema(mergedSchemas);
202
- const data = await generateTypes(app.options.graphql?.framework || "graphql-yoga", schema, app.options.graphql?.codegen?.server ?? {});
225
+ const data = await generateTypes(app.options.graphql?.framework || "graphql-yoga", schema, app.options.graphql ?? {});
203
226
  const printSchema = printSchemaWithDirectives(schema);
204
227
  const schemaPath = resolve(app.graphql.buildDir, "schema.graphql");
205
228
  mkdirSync(dirname(schemaPath), { recursive: true });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nitro-graphql",
3
3
  "type": "module",
4
- "version": "1.3.0",
4
+ "version": "1.4.0",
5
5
  "description": "GraphQL integration for Nitro",
6
6
  "license": "MIT",
7
7
  "sideEffects": false,
@@ -56,6 +56,7 @@
56
56
  ],
57
57
  "peerDependencies": {
58
58
  "@apollo/server": "^5.0.0",
59
+ "@apollo/subgraph": "^2.0.0",
59
60
  "@apollo/utils.withrequired": "^3.0.0",
60
61
  "@as-integrations/h3": "^2.0.0",
61
62
  "graphql": "^16.11.0",
@@ -66,6 +67,9 @@
66
67
  "@apollo/server": {
67
68
  "optional": true
68
69
  },
70
+ "@apollo/subgraph": {
71
+ "optional": true
72
+ },
69
73
  "@apollo/utils.withrequired": {
70
74
  "optional": true
71
75
  },
@@ -79,7 +83,7 @@
79
83
  "@graphql-codegen/typescript": "^4.1.6",
80
84
  "@graphql-codegen/typescript-generic-sdk": "^4.0.2",
81
85
  "@graphql-codegen/typescript-operations": "^4.6.1",
82
- "@graphql-codegen/typescript-resolvers": "^4.5.1",
86
+ "@graphql-codegen/typescript-resolvers": "^4.5.2",
83
87
  "@graphql-tools/graphql-file-loader": "^8.1.0",
84
88
  "@graphql-tools/load": "^8.1.2",
85
89
  "@graphql-tools/load-files": "^7.0.1",
@@ -94,15 +98,16 @@
94
98
  "graphql-scalars": "^1.24.2",
95
99
  "knitwork": "^1.2.0",
96
100
  "ohash": "^2.0.11",
97
- "oxc-parser": "^0.82.3",
101
+ "oxc-parser": "^0.86.0",
98
102
  "pathe": "^2.0.3",
99
103
  "tinyglobby": "^0.2.14"
100
104
  },
101
105
  "devDependencies": {
102
- "@antfu/eslint-config": "^5.2.1",
103
- "@nuxt/kit": "^4.0.3",
104
- "@nuxt/schema": "^4.0.3",
105
- "@types/node": "^22.18.0",
106
+ "@antfu/eslint-config": "^5.2.2",
107
+ "@apollo/subgraph": "^2.11.2",
108
+ "@nuxt/kit": "^4.1.0",
109
+ "@nuxt/schema": "^4.1.0",
110
+ "@types/node": "^22.18.1",
106
111
  "bumpp": "^10.2.3",
107
112
  "changelogen": "^0.6.2",
108
113
  "crossws": "0.3.5",
@@ -110,7 +115,7 @@
110
115
  "graphql": "^16.11.0",
111
116
  "graphql-yoga": "^5.15.1",
112
117
  "h3": "1.15.3",
113
- "nitropack": "^2.12.4",
118
+ "nitropack": "^2.12.5",
114
119
  "tsdown": "^0.14.2",
115
120
  "typescript": "^5.9.2"
116
121
  },
@@ -122,7 +127,9 @@
122
127
  "dev": "tsdown --watch",
123
128
  "bumpp": "bumpp package.json",
124
129
  "release": "pnpm build && pnpm bumpp && pnpm publish --no-git-checks --access public",
125
- "playground": "cd playground && pnpm install && pnpm dev",
130
+ "playground:nitro": "cd playgrounds/nitro && pnpm install && pnpm dev",
131
+ "playground:nuxt": "cd playgrounds/nuxt && pnpm install && pnpm dev",
132
+ "playground:federation": "cd playgrounds/federation && pnpm install && pnpm dev",
126
133
  "lint": "eslint .",
127
134
  "lint:fix": "eslint . --fix"
128
135
  }