hadars 0.2.2-rc.0 → 0.2.2-rc.2

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
@@ -19,30 +19,30 @@ Bring your own router (or none), keep your components as plain React, and get SS
19
19
  ## Benchmarks
20
20
 
21
21
  <!-- BENCHMARK_START -->
22
- > Last run: 2026-03-21 · 60s · 100 connections · Bun runtime
22
+ > Last run: 2026-03-23 · 120s · 100 connections · Bun runtime
23
23
  > hadars is **8.7x faster** in requests/sec
24
24
 
25
- **Throughput** (autocannon, 60s)
25
+ **Throughput** (autocannon, 120s)
26
26
 
27
27
  | Metric | hadars | Next.js |
28
28
  |---|---:|---:|
29
29
  | Requests/sec | **148** | 17 |
30
- | Latency median | **647 ms** | 2712 ms |
31
- | Latency p99 | **969 ms** | 6544 ms |
32
- | Throughput | **42.23** MB/s | 9.29 MB/s |
33
- | Peak RSS | 1062.8 MB | **472.3 MB** |
34
- | Avg RSS | 799.7 MB | **404.7 MB** |
30
+ | Latency median | **639 ms** | 2754 ms |
31
+ | Latency p99 | **976 ms** | 4040 ms |
32
+ | Throughput | **42.23** MB/s | 9.49 MB/s |
33
+ | Peak RSS | 987.9 MB | **476.9 MB** |
34
+ | Avg RSS | 775.3 MB | **424.5 MB** |
35
35
  | Build time | 0.7 s | 6.0 s |
36
36
 
37
37
  **Page load** (Playwright · Chromium headless · median)
38
38
 
39
39
  | Metric | hadars | Next.js |
40
40
  |---|---:|---:|
41
- | TTFB | **20 ms** | 43 ms |
42
- | FCP | **100 ms** | 136 ms |
43
- | DOMContentLoaded | **41 ms** | 127 ms |
44
- | Load | **127 ms** | 174 ms |
45
- | Peak RSS | 444.7 MB | **284.9 MB** |
41
+ | TTFB | **19 ms** | 46 ms |
42
+ | FCP | **96 ms** | 140 ms |
43
+ | DOMContentLoaded | **40 ms** | 128 ms |
44
+ | Load | **124 ms** | 174 ms |
45
+ | Peak RSS | 468.0 MB | **297.0 MB** |
46
46
  <!-- BENCHMARK_END -->
47
47
 
48
48
  ## Quick start
@@ -117,6 +117,9 @@ hadars run
117
117
  # Pre-render every page to static HTML files (output goes to out/ by default)
118
118
  hadars export static [outDir]
119
119
 
120
+ # Write the inferred GraphQL schema to a SDL file (for graphql-codegen / gql.tada)
121
+ hadars export schema [schema.graphql]
122
+
120
123
  # Bundle the app into a single self-contained Lambda .mjs file
121
124
  hadars export lambda [output.mjs]
122
125
 
@@ -355,6 +358,119 @@ For each node type (e.g. `BlogPost`) you get two root queries:
355
358
 
356
359
  All scalar fields are automatically added as optional lookup arguments, so you can do `blogPost(slug: "hello")` without knowing the hashed node id.
357
360
 
361
+ ### useGraphQL hook
362
+
363
+ Query your GraphQL layer directly inside any component — no need to pass data down through `getInitProps`. The hook integrates with `useServerData` so queries are executed on the server during static export and hydrated on the client.
364
+
365
+ ```tsx
366
+ import { useGraphQL } from 'hadars';
367
+ import { GetAllPostsDocument } from './gql/graphql';
368
+
369
+ const PostList = () => {
370
+ const result = useGraphQL(GetAllPostsDocument);
371
+ const posts = result?.data?.allBlogPost ?? [];
372
+ return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
373
+ };
374
+ ```
375
+
376
+ Pass variables as a second argument:
377
+
378
+ ```tsx
379
+ const PostPage = ({ slug }: { slug: string }) => {
380
+ const result = useGraphQL(GetPostDocument, { slug });
381
+ const post = result?.data?.blogPost;
382
+ if (!post) return null;
383
+ return <h1>{post.title}</h1>;
384
+ };
385
+ ```
386
+
387
+ - `result` is `undefined` on the first SSR pass (data not yet resolved) — render `null` or a skeleton
388
+ - When a typed `DocumentNode` from graphql-codegen is passed, the return type is fully inferred — `result.data` has the exact shape of your query
389
+ - GraphQL errors throw during static export so the page is marked as failed rather than silently serving incomplete data
390
+ - Requires `graphql` and a `sources` or `graphql` executor configured in `hadars.config.ts`
391
+
392
+ ### Schema export & type generation
393
+
394
+ Run `hadars export schema` to write the inferred schema to a SDL file, then feed it to **graphql-codegen** to generate TypeScript types for your queries:
395
+
396
+ ```bash
397
+ # 1. Generate schema.graphql from your sources
398
+ hadars export schema
399
+
400
+ # 2. Install codegen (one-time)
401
+ npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
402
+
403
+ # 3. Generate types
404
+ npx graphql-codegen --schema schema.graphql --documents "src/**/*.tsx" --out src/gql/
405
+ ```
406
+
407
+ Or use a `codegen.ts` config file:
408
+
409
+ ```ts
410
+ // codegen.ts
411
+ import type { CodegenConfig } from '@graphql-codegen/cli';
412
+
413
+ const config: CodegenConfig = {
414
+ schema: 'schema.graphql',
415
+ documents: ['src/**/*.tsx'],
416
+ generates: {
417
+ 'src/gql/': {
418
+ preset: 'client',
419
+ },
420
+ },
421
+ };
422
+
423
+ export default config;
424
+ ```
425
+
426
+ `hadars export schema` also works with a custom `graphql` executor — it runs an introspection query against it and converts the result to SDL.
427
+
428
+ ### GraphQL fragments
429
+
430
+ graphql-codegen's `client` preset generates fragment masking helpers (`FragmentType`, `useFragment`, `makeFragmentData`) that let components co-locate their exact data requirements. No hadars changes are needed — just define your fragment with `graphql()` and accept a masked prop:
431
+
432
+ ```tsx
433
+ // src/PostCard.tsx
434
+ import { graphql, useFragment, type FragmentType } from './gql';
435
+
436
+ export const PostCardFragment = graphql(`
437
+ fragment PostCard on BlogPost {
438
+ slug
439
+ title
440
+ date
441
+ }
442
+ `);
443
+
444
+ interface Props {
445
+ post: FragmentType<typeof PostCardFragment>;
446
+ }
447
+
448
+ const PostCard = ({ post: postRef }: Props) => {
449
+ const post = useFragment(PostCardFragment, postRef);
450
+ return (
451
+ <article>
452
+ <h2>{post.title}</h2>
453
+ <time>{post.date}</time>
454
+ </article>
455
+ );
456
+ };
457
+ ```
458
+
459
+ The parent component spreads the raw node into the masked prop — TypeScript ensures it satisfies the fragment shape:
460
+
461
+ ```tsx
462
+ const PostList = () => {
463
+ const result = useGraphQL(GetAllPostsDocument);
464
+ return (
465
+ <>
466
+ {result?.data?.allBlogPost.map(post => (
467
+ <PostCard key={post.slug} post={post} />
468
+ ))}
469
+ </>
470
+ );
471
+ };
472
+ ```
473
+
358
474
  ### Custom GraphQL executor
359
475
 
360
476
  Skip `sources` and provide a `graphql` executor directly for full control over resolvers:
package/cli-lib.ts CHANGED
@@ -6,7 +6,7 @@ import * as Hadars from './src/build'
6
6
  import type { HadarsOptions, HadarsEntryModule } from './src/types/hadars'
7
7
  import { renderStaticSite } from './src/static'
8
8
  import { runSources } from './src/source/runner'
9
- import { buildSchemaExecutor } from './src/source/inference'
9
+ import { buildSchemaExecutor, buildSchemaSDL, introspectExecutorSDL } from './src/source/inference'
10
10
 
11
11
  const SUPPORTED = ['hadars.config.js', 'hadars.config.mjs', 'hadars.config.cjs', 'hadars.config.ts']
12
12
 
@@ -46,6 +46,53 @@ async function loadConfig(configPath: string): Promise<HadarsOptions> {
46
46
  return (mod && (mod.default ?? mod)) as HadarsOptions
47
47
  }
48
48
 
49
+ // ── hadars export schema ─────────────────────────────────────────────────────
50
+
51
+ async function exportSchema(
52
+ config: HadarsOptions,
53
+ outputFile: string,
54
+ ): Promise<void> {
55
+ let sdl: string | null = null
56
+
57
+ if (config.sources && config.sources.length > 0) {
58
+ console.log(`Running ${config.sources.length} source plugin(s)...`)
59
+ const store = await runSources(config.sources)
60
+ console.log(`Schema inferred for types: ${store.getTypes().join(', ') || '(none)'}`)
61
+ sdl = await buildSchemaSDL(store)
62
+ if (!sdl) {
63
+ console.error(
64
+ 'Error: `graphql` package not found.\n' +
65
+ 'Source plugins require graphql-js to be installed:\n\n' +
66
+ ' npm install graphql\n',
67
+ )
68
+ process.exit(1)
69
+ }
70
+ } else if (config.graphql) {
71
+ console.log('Introspecting custom GraphQL executor...')
72
+ sdl = await introspectExecutorSDL(config.graphql)
73
+ if (!sdl) {
74
+ console.error(
75
+ 'Error: `graphql` package not found.\n' +
76
+ 'Schema export requires graphql-js to be installed:\n\n' +
77
+ ' npm install graphql\n',
78
+ )
79
+ process.exit(1)
80
+ }
81
+ } else {
82
+ console.error(
83
+ 'Error: no GraphQL source configured.\n' +
84
+ 'Add `sources` or a `graphql` executor to your hadars.config.ts first.\n',
85
+ )
86
+ process.exit(1)
87
+ }
88
+
89
+ await writeFile(outputFile, sdl, 'utf-8')
90
+ console.log(`Schema written to ${outputFile}`)
91
+ console.log(`\nNext steps — generate TypeScript types with graphql-codegen:`)
92
+ console.log(` npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations`)
93
+ console.log(` npx graphql-codegen --schema ${outputFile} --documents "src/**/*.tsx" --out src/gql/`)
94
+ }
95
+
49
96
  // ── hadars export static ─────────────────────────────────────────────────────
50
97
 
51
98
  async function exportStatic(
@@ -548,7 +595,7 @@ Done! Next steps:
548
595
  // ── CLI entry ─────────────────────────────────────────────────────────────────
549
596
 
550
597
  function usage(): void {
551
- console.log('Usage: hadars <new <name> | dev | build | run | export lambda [output.mjs] | export cloudflare [output.mjs] | export static [outDir]>')
598
+ console.log('Usage: hadars <new <name> | dev | build | run | export lambda [output.mjs] | export cloudflare [output.mjs] | export static [outDir] | export schema [schema.graphql]>')
552
599
  }
553
600
 
554
601
  export async function runCli(argv: string[], cwd = process.cwd()): Promise<void> {
@@ -608,8 +655,12 @@ export async function runCli(argv: string[], cwd = process.cwd()): Promise<void>
608
655
  const outDirArg = argv[4] ?? 'out'
609
656
  await exportStatic(cfg, outDirArg, cwd)
610
657
  process.exit(0)
658
+ } else if (subCmd === 'schema') {
659
+ const outputFile = resolve(cwd, argv[4] ?? 'schema.graphql')
660
+ await exportSchema(cfg, outputFile)
661
+ process.exit(0)
611
662
  } else {
612
- console.error(`Unknown export target: ${subCmd ?? '(none)'}. Supported: lambda, cloudflare, static`)
663
+ console.error(`Unknown export target: ${subCmd ?? '(none)'}. Supported: lambda, cloudflare, static, schema`)
613
664
  process.exit(1)
614
665
  }
615
666
  }
package/dist/cli.js CHANGED
@@ -1905,6 +1905,8 @@ var NodeStore = class {
1905
1905
  byId = /* @__PURE__ */ new Map();
1906
1906
  byType = /* @__PURE__ */ new Map();
1907
1907
  createNode(node) {
1908
+ if (!node.id) throw new Error("[hadars] createNode: node.id must be a non-empty string");
1909
+ if (!node.internal?.type) throw new Error("[hadars] createNode: node.internal.type must be a non-empty string");
1908
1910
  this.byId.set(node.id, node);
1909
1911
  const list = this.byType.get(node.internal.type) ?? [];
1910
1912
  const idx = list.findIndex((n) => n.id === node.id);
@@ -1966,6 +1968,7 @@ function makeGatsbyContext(store, pluginName, pluginOptions = {}, emitter = new
1966
1968
  const actions = {
1967
1969
  createNode: (node) => store.createNode(node),
1968
1970
  deleteNode: ({ id }) => {
1971
+ console.warn(`[${pluginName}] deleteNode("${id}") called but is not supported by hadars \u2014 node will remain in the store.`);
1969
1972
  },
1970
1973
  touchNode: () => {
1971
1974
  }
@@ -1995,6 +1998,7 @@ function makeGatsbyContext(store, pluginName, pluginOptions = {}, emitter = new
1995
1998
  // src/source/runner.ts
1996
1999
  var SETTLE_IDLE_MS = 300;
1997
2000
  var SETTLE_TIMEOUT_MS = 1e4;
2001
+ var SOURCE_NODES_TIMEOUT_MS = 3e4;
1998
2002
  function waitForSettle(getLastNodeTime) {
1999
2003
  return new Promise((resolve2) => {
2000
2004
  const deadline = Date.now() + SETTLE_TIMEOUT_MS;
@@ -2003,10 +2007,19 @@ function waitForSettle(getLastNodeTime) {
2003
2007
  if (idle >= SETTLE_IDLE_MS || Date.now() >= deadline) {
2004
2008
  resolve2();
2005
2009
  } else {
2006
- setTimeout(check, 50);
2010
+ const t2 = setTimeout(check, 50);
2011
+ t2.unref?.();
2007
2012
  }
2008
2013
  };
2009
- setTimeout(check, SETTLE_IDLE_MS);
2014
+ const t = setTimeout(check, SETTLE_IDLE_MS);
2015
+ t.unref?.();
2016
+ });
2017
+ }
2018
+ function withTimeout(promise, ms, label) {
2019
+ return new Promise((resolve2, reject) => {
2020
+ const t = setTimeout(() => reject(new Error(`[hadars] "${label}" timed out after ${ms}ms`)), ms);
2021
+ t.unref?.();
2022
+ promise.then(resolve2, reject).finally(() => clearTimeout(t));
2010
2023
  });
2011
2024
  }
2012
2025
  async function runSources(sources) {
@@ -2047,11 +2060,18 @@ async function runSources(sources) {
2047
2060
  const emitter = new EventEmitter2();
2048
2061
  const ctx = makeGatsbyContext(trackingStore, pluginName, options, emitter);
2049
2062
  try {
2050
- await mod.sourceNodes(ctx, options);
2063
+ await withTimeout(
2064
+ Promise.resolve(mod.sourceNodes(ctx, options)),
2065
+ SOURCE_NODES_TIMEOUT_MS,
2066
+ `${pluginName} sourceNodes`
2067
+ );
2051
2068
  } catch (err) {
2052
- throw new Error(
2053
- `[hadars] source plugin "${pluginName}" threw during sourceNodes: ${err.message}`
2069
+ const cause = err instanceof Error ? err : new Error(String(err));
2070
+ const wrapped = new Error(
2071
+ `[hadars] source plugin "${pluginName}" threw during sourceNodes: ${cause.message}`
2054
2072
  );
2073
+ wrapped.cause = cause;
2074
+ throw wrapped;
2055
2075
  }
2056
2076
  await waitForSettle(() => lastNodeTime);
2057
2077
  emitter.emit("BOOTSTRAP_FINISHED");
@@ -2080,12 +2100,13 @@ function inferFieldShape(value, seenTypes) {
2080
2100
  return { type: inferScalar(value), nullable: true };
2081
2101
  }
2082
2102
  var INTERNAL_FIELDS = /* @__PURE__ */ new Set(["id", "internal", "__typename", "parent", "children"]);
2103
+ var isReservedFieldName = (name) => name.startsWith("__");
2083
2104
  var FILTERABLE_SCALARS = /* @__PURE__ */ new Set(["String", "Int", "Float", "Boolean", "ID"]);
2084
2105
  function buildTypeFields(nodes) {
2085
2106
  const fieldMap = /* @__PURE__ */ new Map();
2086
2107
  for (const node of nodes) {
2087
2108
  for (const [key, val] of Object.entries(node)) {
2088
- if (INTERNAL_FIELDS.has(key)) continue;
2109
+ if (INTERNAL_FIELDS.has(key) || isReservedFieldName(key)) continue;
2089
2110
  if (fieldMap.has(key)) continue;
2090
2111
  const { type } = inferFieldShape(val, /* @__PURE__ */ new Set());
2091
2112
  fieldMap.set(key, {
@@ -2117,7 +2138,7 @@ function buildSingleArgs(fields) {
2117
2138
  ];
2118
2139
  return args.join(", ");
2119
2140
  }
2120
- async function buildSchemaExecutor(store) {
2141
+ async function loadAndBuildSchema(store) {
2121
2142
  let gql;
2122
2143
  try {
2123
2144
  const { createRequire: createRequire2 } = await import("node:module");
@@ -2127,11 +2148,11 @@ async function buildSchemaExecutor(store) {
2127
2148
  } catch {
2128
2149
  return null;
2129
2150
  }
2130
- const { buildSchema, graphql } = gql;
2151
+ const { buildSchema, printSchema, print } = gql;
2131
2152
  const types = store.getTypes();
2132
2153
  if (types.length === 0) {
2133
- const schema2 = buildSchema("type Query { _empty: String }");
2134
- return (query, variables) => graphql({ schema: schema2, source: query, variableValues: variables });
2154
+ const rawSdl2 = "type Query { _empty: String }";
2155
+ return { schema: buildSchema(rawSdl2), sdl: rawSdl2, gql };
2135
2156
  }
2136
2157
  const typeFields = new Map(
2137
2158
  types.map((typeName) => {
@@ -2150,7 +2171,7 @@ async function buildSchemaExecutor(store) {
2150
2171
  ` ${all}: [${typeName}!]!`
2151
2172
  ].join("\n");
2152
2173
  });
2153
- const sdl = [
2174
+ const rawSdl = [
2154
2175
  ...typeSDLs,
2155
2176
  `type Query {
2156
2177
  ${queryFields.join("\n")}
@@ -2158,10 +2179,24 @@ ${queryFields.join("\n")}
2158
2179
  ].join("\n\n");
2159
2180
  let schema;
2160
2181
  try {
2161
- schema = buildSchema(sdl);
2182
+ schema = buildSchema(rawSdl);
2162
2183
  } catch (err) {
2163
2184
  throw new Error(`[hadars] Failed to build GraphQL schema from node store: ${err.message}`);
2164
2185
  }
2186
+ return { schema, sdl: printSchema(schema), gql };
2187
+ }
2188
+ function toQueryString(query, print) {
2189
+ return typeof query === "string" ? query : print(query);
2190
+ }
2191
+ async function buildSchemaExecutor(store) {
2192
+ const built = await loadAndBuildSchema(store);
2193
+ if (!built) return null;
2194
+ const { schema, gql } = built;
2195
+ const { graphql, print } = gql;
2196
+ const types = store.getTypes();
2197
+ if (types.length === 0) {
2198
+ return (query, variables) => graphql({ schema, source: toQueryString(query, print), variableValues: variables });
2199
+ }
2165
2200
  const rootValue = {};
2166
2201
  for (const typeName of types) {
2167
2202
  const { single, all } = queryNames(typeName);
@@ -2173,7 +2208,31 @@ ${queryFields.join("\n")}
2173
2208
  ) ?? null;
2174
2209
  };
2175
2210
  }
2176
- return (query, variables) => graphql({ schema, rootValue, source: query, variableValues: variables });
2211
+ return (query, variables) => graphql({ schema, rootValue, source: toQueryString(query, print), variableValues: variables });
2212
+ }
2213
+ async function buildSchemaSDL(store) {
2214
+ const built = await loadAndBuildSchema(store);
2215
+ return built?.sdl ?? null;
2216
+ }
2217
+ async function introspectExecutorSDL(executor) {
2218
+ let gql;
2219
+ try {
2220
+ const { createRequire: createRequire2 } = await import("node:module");
2221
+ const projectRequire = createRequire2(process.cwd() + "/package.json");
2222
+ const graphqlPath = projectRequire.resolve("graphql");
2223
+ gql = await import(graphqlPath);
2224
+ } catch {
2225
+ return null;
2226
+ }
2227
+ const { getIntrospectionQuery, buildClientSchema, printSchema } = gql;
2228
+ const result = await executor(getIntrospectionQuery());
2229
+ if (result.errors?.length) {
2230
+ throw new Error(`[hadars] Introspection failed: ${result.errors[0].message}`);
2231
+ }
2232
+ if (!result.data) {
2233
+ throw new Error("[hadars] Introspection returned no data");
2234
+ }
2235
+ return printSchema(buildClientSchema(result.data));
2177
2236
  }
2178
2237
 
2179
2238
  // src/source/graphiql.ts
@@ -2208,7 +2267,12 @@ var GRAPHIQL_HTML = `<!doctype html>
2208
2267
  </html>`;
2209
2268
  function createGraphiqlHandler(executor) {
2210
2269
  return async (req) => {
2211
- const url = new URL(req.url);
2270
+ let url;
2271
+ try {
2272
+ url = new URL(req.url);
2273
+ } catch {
2274
+ return void 0;
2275
+ }
2212
2276
  if (url.pathname !== GRAPHQL_PATH) return void 0;
2213
2277
  if (req.method === "GET") {
2214
2278
  return new Response(GRAPHIQL_HTML, {
@@ -2225,8 +2289,8 @@ function createGraphiqlHandler(executor) {
2225
2289
  headers: { "Content-Type": "application/json" }
2226
2290
  });
2227
2291
  }
2228
- if (!body.query) {
2229
- return new Response(JSON.stringify({ errors: [{ message: 'Missing "query" field' }] }), {
2292
+ if (typeof body.query !== "string" || !body.query.trim()) {
2293
+ return new Response(JSON.stringify({ errors: [{ message: 'Missing or invalid "query" field \u2014 must be a non-empty string' }] }), {
2230
2294
  status: 400,
2231
2295
  headers: { "Content-Type": "application/json" }
2232
2296
  });
@@ -2659,6 +2723,7 @@ var dev = async (options) => {
2659
2723
  getInitProps,
2660
2724
  getFinalProps
2661
2725
  } = await import(importPath);
2726
+ globalThis.__hadarsGraphQL = devStaticCtx?.graphql;
2662
2727
  const { head, status, getAppBody, finalize } = await getReactResponse(request, {
2663
2728
  document: {
2664
2729
  body: Component,
@@ -2864,6 +2929,7 @@ async function renderStaticSite(opts) {
2864
2929
  for (const urlPath of paths) {
2865
2930
  try {
2866
2931
  const req = parseRequest(new Request("http://localhost" + urlPath));
2932
+ globalThis.__hadarsGraphQL = staticCtx.graphql;
2867
2933
  const { head, getAppBody, finalize } = await getReactResponse(req, {
2868
2934
  document: {
2869
2935
  body: ssrModule.default,
@@ -2937,6 +3003,41 @@ async function loadConfig(configPath) {
2937
3003
  const mod = await import(url);
2938
3004
  return mod && (mod.default ?? mod);
2939
3005
  }
3006
+ async function exportSchema(config, outputFile) {
3007
+ let sdl = null;
3008
+ if (config.sources && config.sources.length > 0) {
3009
+ console.log(`Running ${config.sources.length} source plugin(s)...`);
3010
+ const store = await runSources(config.sources);
3011
+ console.log(`Schema inferred for types: ${store.getTypes().join(", ") || "(none)"}`);
3012
+ sdl = await buildSchemaSDL(store);
3013
+ if (!sdl) {
3014
+ console.error(
3015
+ "Error: `graphql` package not found.\nSource plugins require graphql-js to be installed:\n\n npm install graphql\n"
3016
+ );
3017
+ process.exit(1);
3018
+ }
3019
+ } else if (config.graphql) {
3020
+ console.log("Introspecting custom GraphQL executor...");
3021
+ sdl = await introspectExecutorSDL(config.graphql);
3022
+ if (!sdl) {
3023
+ console.error(
3024
+ "Error: `graphql` package not found.\nSchema export requires graphql-js to be installed:\n\n npm install graphql\n"
3025
+ );
3026
+ process.exit(1);
3027
+ }
3028
+ } else {
3029
+ console.error(
3030
+ "Error: no GraphQL source configured.\nAdd `sources` or a `graphql` executor to your hadars.config.ts first.\n"
3031
+ );
3032
+ process.exit(1);
3033
+ }
3034
+ await writeFile2(outputFile, sdl, "utf-8");
3035
+ console.log(`Schema written to ${outputFile}`);
3036
+ console.log(`
3037
+ Next steps \u2014 generate TypeScript types with graphql-codegen:`);
3038
+ console.log(` npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations`);
3039
+ console.log(` npx graphql-codegen --schema ${outputFile} --documents "src/**/*.tsx" --out src/gql/`);
3040
+ }
2940
3041
  async function exportStatic(config, outputDir, cwd) {
2941
3042
  if (!config.paths) {
2942
3043
  console.error(
@@ -3369,7 +3470,7 @@ Done! Next steps:
3369
3470
  `);
3370
3471
  }
3371
3472
  function usage() {
3372
- console.log("Usage: hadars <new <name> | dev | build | run | export lambda [output.mjs] | export cloudflare [output.mjs] | export static [outDir]>");
3473
+ console.log("Usage: hadars <new <name> | dev | build | run | export lambda [output.mjs] | export cloudflare [output.mjs] | export static [outDir] | export schema [schema.graphql]>");
3373
3474
  }
3374
3475
  async function runCli(argv, cwd = process.cwd()) {
3375
3476
  const cmd = argv[2];
@@ -3424,8 +3525,12 @@ async function runCli(argv, cwd = process.cwd()) {
3424
3525
  const outDirArg = argv[4] ?? "out";
3425
3526
  await exportStatic(cfg, outDirArg, cwd);
3426
3527
  process.exit(0);
3528
+ } else if (subCmd === "schema") {
3529
+ const outputFile = resolve(cwd, argv[4] ?? "schema.graphql");
3530
+ await exportSchema(cfg, outputFile);
3531
+ process.exit(0);
3427
3532
  } else {
3428
- console.error(`Unknown export target: ${subCmd ?? "(none)"}. Supported: lambda, cloudflare, static`);
3533
+ console.error(`Unknown export target: ${subCmd ?? "(none)"}. Supported: lambda, cloudflare, static, schema`);
3429
3534
  process.exit(1);
3430
3535
  }
3431
3536
  }
@@ -1,4 +1,4 @@
1
- import { H as HadarsEntryModule, a as HadarsOptions } from './hadars-mKu5txjW.cjs';
1
+ import { H as HadarsEntryModule, a as HadarsOptions } from './hadars-CSWWhlQC.cjs';
2
2
 
3
3
  /**
4
4
  * Cloudflare Workers adapter for hadars.
@@ -1,4 +1,4 @@
1
- import { H as HadarsEntryModule, a as HadarsOptions } from './hadars-mKu5txjW.js';
1
+ import { H as HadarsEntryModule, a as HadarsOptions } from './hadars-CSWWhlQC.js';
2
2
 
3
3
  /**
4
4
  * Cloudflare Workers adapter for hadars.
@@ -1,3 +1,22 @@
1
+ /**
2
+ * Minimal structural representation of a typed GraphQL document node.
3
+ *
4
+ * Compatible with `TypedDocumentNode` from `@graphql-typed-document-node/core`
5
+ * and the documents emitted by graphql-codegen's `client` preset — so passing
6
+ * a generated document object gives you fully-inferred result and variable types
7
+ * without writing explicit generics.
8
+ *
9
+ * hadars intentionally avoids importing from `graphql` or
10
+ * `@graphql-typed-document-node/core` so that neither is a required dependency.
11
+ */
12
+ interface HadarsDocumentNode<TResult = Record<string, unknown>, TVariables = Record<string, unknown>> {
13
+ /** @internal Used by TypeScript to carry the result type. */
14
+ readonly __apiType?: (variables: TVariables) => TResult;
15
+ /** At least one definition — ensures a plain string is not assignable. */
16
+ readonly definitions: ReadonlyArray<{
17
+ readonly kind: string;
18
+ }>;
19
+ }
1
20
  /**
2
21
  * In-process GraphQL executor passed to `getInitProps` and `paths` during
3
22
  * `hadars export static`. Hadars is executor-agnostic — configure it in
@@ -13,9 +32,24 @@
13
32
  * gql({ schema, rootValue, source: query, variableValues: variables }),
14
33
  * } satisfies HadarsOptions;
15
34
  * ```
35
+ *
36
+ * The executor is generic — call it with explicit type parameters or pass a
37
+ * `TypedDocumentNode` / codegen-generated document to get inferred types:
38
+ *
39
+ * ```ts
40
+ * // Explicit generics:
41
+ * const { data } = await ctx.graphql<GetPostQuery, GetPostQueryVariables>(
42
+ * `query GetPost($slug: String) { blogPost(slug: $slug) { title } }`,
43
+ * { slug },
44
+ * );
45
+ *
46
+ * // Inferred via TypedDocumentNode (graphql-codegen client preset):
47
+ * import { GetPostDocument } from './gql';
48
+ * const { data } = await ctx.graphql(GetPostDocument, { slug });
49
+ * ```
16
50
  */
17
- type GraphQLExecutor = (query: string, variables?: Record<string, unknown>) => Promise<{
18
- data?: any;
51
+ type GraphQLExecutor = <TData = any, TVariables extends Record<string, unknown> = Record<string, unknown>>(query: string | HadarsDocumentNode<TData, TVariables>, variables?: TVariables) => Promise<{
52
+ data?: TData;
19
53
  errors?: ReadonlyArray<{
20
54
  message: string;
21
55
  }>;
@@ -252,4 +286,4 @@ interface HadarsRequest extends Request {
252
286
  cookies: Record<string, string>;
253
287
  }
254
288
 
255
- export type { GraphQLExecutor as G, HadarsEntryModule as H, HadarsOptions as a, HadarsApp as b, HadarsGetClientProps as c, HadarsGetFinalProps as d, HadarsGetInitialProps as e, HadarsProps as f, HadarsRequest as g, HadarsSourceEntry as h, HadarsStaticContext as i };
289
+ export type { GraphQLExecutor as G, HadarsEntryModule as H, HadarsOptions as a, HadarsDocumentNode as b, HadarsApp as c, HadarsGetClientProps as d, HadarsGetFinalProps as e, HadarsGetInitialProps as f, HadarsProps as g, HadarsRequest as h, HadarsSourceEntry as i, HadarsStaticContext as j };
@@ -1,3 +1,22 @@
1
+ /**
2
+ * Minimal structural representation of a typed GraphQL document node.
3
+ *
4
+ * Compatible with `TypedDocumentNode` from `@graphql-typed-document-node/core`
5
+ * and the documents emitted by graphql-codegen's `client` preset — so passing
6
+ * a generated document object gives you fully-inferred result and variable types
7
+ * without writing explicit generics.
8
+ *
9
+ * hadars intentionally avoids importing from `graphql` or
10
+ * `@graphql-typed-document-node/core` so that neither is a required dependency.
11
+ */
12
+ interface HadarsDocumentNode<TResult = Record<string, unknown>, TVariables = Record<string, unknown>> {
13
+ /** @internal Used by TypeScript to carry the result type. */
14
+ readonly __apiType?: (variables: TVariables) => TResult;
15
+ /** At least one definition — ensures a plain string is not assignable. */
16
+ readonly definitions: ReadonlyArray<{
17
+ readonly kind: string;
18
+ }>;
19
+ }
1
20
  /**
2
21
  * In-process GraphQL executor passed to `getInitProps` and `paths` during
3
22
  * `hadars export static`. Hadars is executor-agnostic — configure it in
@@ -13,9 +32,24 @@
13
32
  * gql({ schema, rootValue, source: query, variableValues: variables }),
14
33
  * } satisfies HadarsOptions;
15
34
  * ```
35
+ *
36
+ * The executor is generic — call it with explicit type parameters or pass a
37
+ * `TypedDocumentNode` / codegen-generated document to get inferred types:
38
+ *
39
+ * ```ts
40
+ * // Explicit generics:
41
+ * const { data } = await ctx.graphql<GetPostQuery, GetPostQueryVariables>(
42
+ * `query GetPost($slug: String) { blogPost(slug: $slug) { title } }`,
43
+ * { slug },
44
+ * );
45
+ *
46
+ * // Inferred via TypedDocumentNode (graphql-codegen client preset):
47
+ * import { GetPostDocument } from './gql';
48
+ * const { data } = await ctx.graphql(GetPostDocument, { slug });
49
+ * ```
16
50
  */
17
- type GraphQLExecutor = (query: string, variables?: Record<string, unknown>) => Promise<{
18
- data?: any;
51
+ type GraphQLExecutor = <TData = any, TVariables extends Record<string, unknown> = Record<string, unknown>>(query: string | HadarsDocumentNode<TData, TVariables>, variables?: TVariables) => Promise<{
52
+ data?: TData;
19
53
  errors?: ReadonlyArray<{
20
54
  message: string;
21
55
  }>;
@@ -252,4 +286,4 @@ interface HadarsRequest extends Request {
252
286
  cookies: Record<string, string>;
253
287
  }
254
288
 
255
- export type { GraphQLExecutor as G, HadarsEntryModule as H, HadarsOptions as a, HadarsApp as b, HadarsGetClientProps as c, HadarsGetFinalProps as d, HadarsGetInitialProps as e, HadarsProps as f, HadarsRequest as g, HadarsSourceEntry as h, HadarsStaticContext as i };
289
+ export type { GraphQLExecutor as G, HadarsEntryModule as H, HadarsOptions as a, HadarsDocumentNode as b, HadarsApp as c, HadarsGetClientProps as d, HadarsGetFinalProps as e, HadarsGetInitialProps as f, HadarsProps as g, HadarsRequest as h, HadarsSourceEntry as i, HadarsStaticContext as j };