hadars 0.2.2-rc.1 → 0.3.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 +130 -14
- package/cli-lib.ts +54 -3
- package/dist/cli.js +123 -18
- package/dist/cloudflare.d.cts +1 -1
- package/dist/cloudflare.d.ts +1 -1
- package/dist/{hadars-mKu5txjW.d.cts → hadars-CSWWhlQC.d.cts} +37 -3
- package/dist/{hadars-mKu5txjW.d.ts → hadars-CSWWhlQC.d.ts} +37 -3
- package/dist/index.cjs +30 -1
- package/dist/index.d.cts +29 -2
- package/dist/index.d.ts +29 -2
- package/dist/index.js +29 -1
- package/dist/lambda.d.cts +1 -1
- package/dist/lambda.d.ts +1 -1
- package/dist/utils/Head.tsx +95 -2
- package/package.json +1 -1
- package/src/build.ts +3 -0
- package/src/index.tsx +2 -1
- package/src/source/context.ts +5 -5
- package/src/source/graphiql.ts +8 -3
- package/src/source/inference.ts +92 -20
- package/src/source/runner.ts +29 -5
- package/src/source/store.ts +2 -0
- package/src/static.ts +3 -0
- package/src/types/hadars.ts +43 -4
- package/src/utils/Head.tsx +95 -2
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-
|
|
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,
|
|
25
|
+
**Throughput** (autocannon, 120s)
|
|
26
26
|
|
|
27
27
|
| Metric | hadars | Next.js |
|
|
28
28
|
|---|---:|---:|
|
|
29
|
-
| Requests/sec | **
|
|
30
|
-
| Latency median | **
|
|
31
|
-
| Latency p99 | **
|
|
32
|
-
| Throughput | **
|
|
33
|
-
| Peak RSS |
|
|
34
|
-
| Avg RSS |
|
|
35
|
-
| Build time | 0.
|
|
29
|
+
| Requests/sec | **165** | 19 |
|
|
30
|
+
| Latency median | **599 ms** | 2621 ms |
|
|
31
|
+
| Latency p99 | **982 ms** | 3716 ms |
|
|
32
|
+
| Throughput | **46.99** MB/s | 10.37 MB/s |
|
|
33
|
+
| Peak RSS | 1038.2 MB | **489.6 MB** |
|
|
34
|
+
| Avg RSS | 793.4 MB | **432.4 MB** |
|
|
35
|
+
| Build time | 0.8 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 | **
|
|
42
|
-
| FCP | **
|
|
43
|
-
| DOMContentLoaded | **
|
|
44
|
-
| Load | **
|
|
45
|
-
| Peak RSS |
|
|
41
|
+
| TTFB | **18 ms** | 38 ms |
|
|
42
|
+
| FCP | **96 ms** | 128 ms |
|
|
43
|
+
| DOMContentLoaded | **39 ms** | 118 ms |
|
|
44
|
+
| Load | **122 ms** | 164 ms |
|
|
45
|
+
| Peak RSS | 496.7 MB | **304.8 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
|
|
2063
|
+
await withTimeout(
|
|
2064
|
+
Promise.resolve(mod.sourceNodes(ctx, options)),
|
|
2065
|
+
SOURCE_NODES_TIMEOUT_MS,
|
|
2066
|
+
`${pluginName} sourceNodes`
|
|
2067
|
+
);
|
|
2051
2068
|
} catch (err) {
|
|
2052
|
-
|
|
2053
|
-
|
|
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
|
|
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,
|
|
2151
|
+
const { buildSchema, printSchema, print } = gql;
|
|
2131
2152
|
const types = store.getTypes();
|
|
2132
2153
|
if (types.length === 0) {
|
|
2133
|
-
const
|
|
2134
|
-
return
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
}
|
package/dist/cloudflare.d.cts
CHANGED
package/dist/cloudflare.d.ts
CHANGED
|
@@ -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 =
|
|
18
|
-
data?:
|
|
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,
|
|
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 =
|
|
18
|
-
data?:
|
|
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,
|
|
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 };
|