hadars 0.2.1 → 0.2.2-rc.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.
@@ -0,0 +1,96 @@
1
+ /**
2
+ * GraphiQL dev endpoint — serves the GraphiQL IDE at GET /__hadars/graphql
3
+ * and a JSON GraphQL API at POST /__hadars/graphql.
4
+ *
5
+ * Only mounted in dev mode when config.sources is present.
6
+ */
7
+
8
+ import type { GraphQLExecutor } from '../types/hadars';
9
+
10
+ export const GRAPHQL_PATH = '/__hadars/graphql';
11
+
12
+ // GraphiQL HTML shell — UMD bundles from unpkg (dev-only).
13
+ // Using UMD avoids ES module peer-dependency conflicts (CodeMirror, etc.)
14
+ // and matches the official GraphiQL standalone embedding guide.
15
+ const GRAPHIQL_HTML = `<!doctype html>
16
+ <html lang="en">
17
+ <head>
18
+ <meta charset="utf-8">
19
+ <meta name="viewport" content="width=device-width, initial-scale=1">
20
+ <title>GraphiQL — hadars</title>
21
+ <style>
22
+ * { box-sizing: border-box; margin: 0; padding: 0; }
23
+ body { height: 100vh; overflow: hidden; }
24
+ #graphiql { height: 100vh; }
25
+ </style>
26
+ <link rel="stylesheet" href="https://unpkg.com/graphiql@3/graphiql.min.css" />
27
+ </head>
28
+ <body>
29
+ <div id="graphiql"></div>
30
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
31
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
32
+ <script src="https://unpkg.com/graphiql@3/graphiql.min.js" crossorigin></script>
33
+ <script>
34
+ const root = ReactDOM.createRoot(document.getElementById('graphiql'));
35
+ root.render(
36
+ React.createElement(GraphiQL, {
37
+ fetcher: GraphiQL.createFetcher({ url: '${GRAPHQL_PATH}' }),
38
+ })
39
+ );
40
+ </script>
41
+ </body>
42
+ </html>`;
43
+
44
+ /**
45
+ * Returns a fetch handler that covers GET and POST for `/__hadars/graphql`.
46
+ * Returns `undefined` for any other path so callers can chain normally.
47
+ */
48
+ export function createGraphiqlHandler(
49
+ executor: GraphQLExecutor,
50
+ ): (req: Request) => Promise<Response | undefined> {
51
+ return async (req: Request): Promise<Response | undefined> => {
52
+ const url = new URL(req.url);
53
+ if (url.pathname !== GRAPHQL_PATH) return undefined;
54
+
55
+ // GET — serve GraphiQL IDE
56
+ if (req.method === 'GET') {
57
+ return new Response(GRAPHIQL_HTML, {
58
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
59
+ });
60
+ }
61
+
62
+ // POST — execute GraphQL query
63
+ if (req.method === 'POST') {
64
+ let body: { query?: string; variables?: Record<string, unknown>; operationName?: string };
65
+ try {
66
+ body = await req.json();
67
+ } catch {
68
+ return new Response(JSON.stringify({ errors: [{ message: 'Invalid JSON body' }] }), {
69
+ status: 400,
70
+ headers: { 'Content-Type': 'application/json' },
71
+ });
72
+ }
73
+
74
+ if (!body.query) {
75
+ return new Response(JSON.stringify({ errors: [{ message: 'Missing "query" field' }] }), {
76
+ status: 400,
77
+ headers: { 'Content-Type': 'application/json' },
78
+ });
79
+ }
80
+
81
+ try {
82
+ const result = await executor(body.query, body.variables);
83
+ return new Response(JSON.stringify(result), {
84
+ headers: { 'Content-Type': 'application/json' },
85
+ });
86
+ } catch (err) {
87
+ return new Response(JSON.stringify({ errors: [{ message: (err as Error).message }] }), {
88
+ status: 500,
89
+ headers: { 'Content-Type': 'application/json' },
90
+ });
91
+ }
92
+ }
93
+
94
+ return new Response('Method Not Allowed', { status: 405 });
95
+ };
96
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Schema inference — builds a GraphQL schema from the node store automatically,
3
+ * just like Gatsby does. Uses graphql-js (must be installed in the user's project).
4
+ *
5
+ * Dynamically imports graphql so hadars itself does not depend on it.
6
+ */
7
+
8
+ import type { NodeStore } from './store';
9
+ import type { GraphQLExecutor } from '../types/hadars';
10
+
11
+ // ── Primitive inference ────────────────────────────────────────────────────────
12
+
13
+ type ScalarName = 'String' | 'Int' | 'Float' | 'Boolean' | 'ID';
14
+
15
+ function inferScalar(value: unknown): ScalarName {
16
+ if (typeof value === 'boolean') return 'Boolean';
17
+ if (typeof value === 'number') return Number.isInteger(value) ? 'Int' : 'Float';
18
+ return 'String';
19
+ }
20
+
21
+ interface FieldShape {
22
+ type: string; // e.g. "String", "Int", "Boolean", "[String]", "InternalType"
23
+ nullable: boolean;
24
+ }
25
+
26
+ function inferFieldShape(value: unknown, seenTypes: Set<string>): FieldShape {
27
+ if (value === null || value === undefined) {
28
+ return { type: 'String', nullable: true };
29
+ }
30
+ if (Array.isArray(value)) {
31
+ const inner = value.length > 0
32
+ ? inferFieldShape(value[0], seenTypes)
33
+ : { type: 'String', nullable: true };
34
+ return { type: `[${inner.type}]`, nullable: true };
35
+ }
36
+ if (typeof value === 'object') {
37
+ // nested object — we don't recurse deeply for now; use JSON string
38
+ return { type: 'String', nullable: true };
39
+ }
40
+ return { type: inferScalar(value), nullable: true };
41
+ }
42
+
43
+ // ── Schema string builder ──────────────────────────────────────────────────────
44
+
45
+ const INTERNAL_FIELDS = new Set(['id', 'internal', '__typename', 'parent', 'children']);
46
+
47
+ /** Scalar GraphQL types that are safe to use as lookup filter arguments. */
48
+ const FILTERABLE_SCALARS = new Set(['String', 'Int', 'Float', 'Boolean', 'ID']);
49
+
50
+ interface InferredField {
51
+ name: string;
52
+ type: string;
53
+ /** True when the base type is a plain scalar (not a list/object). */
54
+ filterable: boolean;
55
+ }
56
+
57
+ function buildTypeFields(nodes: readonly Record<string, unknown>[]): InferredField[] {
58
+ const fieldMap = new Map<string, InferredField>();
59
+
60
+ for (const node of nodes) {
61
+ for (const [key, val] of Object.entries(node)) {
62
+ if (INTERNAL_FIELDS.has(key)) continue;
63
+ if (fieldMap.has(key)) continue;
64
+ const { type } = inferFieldShape(val, new Set());
65
+ fieldMap.set(key, {
66
+ name: key,
67
+ type,
68
+ filterable: FILTERABLE_SCALARS.has(type),
69
+ });
70
+ }
71
+ }
72
+
73
+ return Array.from(fieldMap.values());
74
+ }
75
+
76
+ function buildTypeSDL(typeName: string, fields: InferredField[]): string {
77
+ const lines = [
78
+ ' id: ID!',
79
+ ...fields.map(f => ` ${f.name}: ${f.type}`),
80
+ ];
81
+ return `type ${typeName} {\n${lines.join('\n')}\n}`;
82
+ }
83
+
84
+ // ── Query builder ──────────────────────────────────────────────────────────────
85
+
86
+ /** Build allXxx / xxx query names from a type name, matching Gatsby's convention. */
87
+ function queryNames(typeName: string) {
88
+ const lower = typeName.charAt(0).toLowerCase() + typeName.slice(1);
89
+ return { single: lower, all: `all${typeName}` };
90
+ }
91
+
92
+ /**
93
+ * Build the SDL argument list for the single-item query.
94
+ * Includes `id` plus every filterable scalar field so callers can look up
95
+ * nodes by any natural key (e.g. slug, email) without knowing the hashed id.
96
+ * The resolver returns the first node where ALL supplied arguments match.
97
+ */
98
+ function buildSingleArgs(fields: InferredField[]): string {
99
+ const args = [
100
+ 'id: ID',
101
+ ...fields.filter(f => f.filterable && f.name !== 'id').map(f => `${f.name}: ${f.type}`),
102
+ ];
103
+ return args.join(', ');
104
+ }
105
+
106
+ // ── Public API ─────────────────────────────────────────────────────────────────
107
+
108
+ /**
109
+ * Build a GraphQL executor backed by the node store.
110
+ *
111
+ * Returns null if graphql-js is not installed — in that case the caller should
112
+ * surface a clear error message asking the user to install `graphql`.
113
+ */
114
+ export async function buildSchemaExecutor(
115
+ store: NodeStore,
116
+ ): Promise<GraphQLExecutor | null> {
117
+ // graphql is an optional peer dependency installed in the user's project,
118
+ // not in hadars itself. Resolve it from process.cwd() so Node.js finds it
119
+ // in the user's node_modules rather than the CLI's own node_modules.
120
+ let gql: any;
121
+ try {
122
+ const { createRequire } = await import('node:module');
123
+ const projectRequire = createRequire(process.cwd() + '/package.json');
124
+ const graphqlPath = projectRequire.resolve('graphql');
125
+ gql = await import(graphqlPath);
126
+ } catch {
127
+ return null;
128
+ }
129
+
130
+ const { buildSchema, graphql } = gql;
131
+
132
+ const types = store.getTypes();
133
+ if (types.length === 0) {
134
+ // Empty store — return a no-op executor with a dummy schema
135
+ const schema = buildSchema('type Query { _empty: String }');
136
+ return (query, variables) => graphql({ schema, source: query, variableValues: variables });
137
+ }
138
+
139
+ // Infer field shapes once per type — reused for both the SDL and resolvers.
140
+ const typeFields = new Map(
141
+ types.map(typeName => {
142
+ const nodes = store.getNodesByType(typeName) as Record<string, unknown>[];
143
+ return [typeName, buildTypeFields(nodes)] as const;
144
+ })
145
+ );
146
+
147
+ const typeSDLs = types.map(typeName =>
148
+ buildTypeSDL(typeName, typeFields.get(typeName)!)
149
+ );
150
+
151
+ const queryFields = types.map(typeName => {
152
+ const { single, all } = queryNames(typeName);
153
+ const args = buildSingleArgs(typeFields.get(typeName)!);
154
+ return [
155
+ ` ${single}(${args}): ${typeName}`,
156
+ ` ${all}: [${typeName}!]!`,
157
+ ].join('\n');
158
+ });
159
+
160
+ const sdl = [
161
+ ...typeSDLs,
162
+ `type Query {\n${queryFields.join('\n')}\n}`,
163
+ ].join('\n\n');
164
+
165
+ let schema: any;
166
+ try {
167
+ schema = buildSchema(sdl);
168
+ } catch (err) {
169
+ throw new Error(`[hadars] Failed to build GraphQL schema from node store: ${(err as Error).message}`);
170
+ }
171
+
172
+ // Build root resolver map
173
+ const rootValue: Record<string, unknown> = {};
174
+ for (const typeName of types) {
175
+ const { single, all } = queryNames(typeName);
176
+ rootValue[all] = () => store.getNodesByType(typeName);
177
+ // Single-item resolver: return the first node matching ALL supplied args.
178
+ rootValue[single] = (args: Record<string, unknown>) => {
179
+ const nodes = store.getNodesByType(typeName) as Record<string, unknown>[];
180
+ return nodes.find(node =>
181
+ Object.entries(args).every(([k, v]) => v === undefined || node[k] === v)
182
+ ) ?? null;
183
+ };
184
+ }
185
+
186
+ return (query, variables) =>
187
+ graphql({ schema, rootValue, source: query, variableValues: variables }) as any;
188
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Source plugin runner — calls each plugin's `sourceNodes` with a Gatsby-compatible
3
+ * context object, then returns a populated node store ready for schema inference.
4
+ *
5
+ * Handles async plugins (e.g. gatsby-source-filesystem) that return immediately
6
+ * and create nodes later via file-system events:
7
+ * 1. Wait for nodes to stop arriving ("settle").
8
+ * 2. Emit BOOTSTRAP_FINISHED on the plugin's emitter.
9
+ * 3. Wait for any post-bootstrap nodes to settle.
10
+ */
11
+
12
+ import { EventEmitter } from 'node:events';
13
+ import { createRequire } from 'node:module';
14
+ import { NodeStore } from './store';
15
+ import { makeGatsbyContext } from './context';
16
+ import type { HadarsSourceEntry } from '../types/hadars';
17
+
18
+ /** ms without a new createNode call before we consider the plugin settled. */
19
+ const SETTLE_IDLE_MS = 300;
20
+ /** Hard timeout — give up waiting after this many ms. */
21
+ const SETTLE_TIMEOUT_MS = 10_000;
22
+
23
+ /**
24
+ * Wait until no createNode call has been received for SETTLE_IDLE_MS,
25
+ * or until SETTLE_TIMEOUT_MS elapses, whichever comes first.
26
+ */
27
+ function waitForSettle(getLastNodeTime: () => number): Promise<void> {
28
+ return new Promise(resolve => {
29
+ const deadline = Date.now() + SETTLE_TIMEOUT_MS;
30
+ const check = () => {
31
+ const idle = Date.now() - getLastNodeTime();
32
+ if (idle >= SETTLE_IDLE_MS || Date.now() >= deadline) {
33
+ resolve();
34
+ } else {
35
+ setTimeout(check, 50);
36
+ }
37
+ };
38
+ setTimeout(check, SETTLE_IDLE_MS);
39
+ });
40
+ }
41
+
42
+ export async function runSources(
43
+ sources: HadarsSourceEntry[],
44
+ ): Promise<NodeStore> {
45
+ const store = new NodeStore();
46
+
47
+ for (const entry of sources) {
48
+ const { resolve, options = {} } = entry;
49
+
50
+ let mod: { sourceNodes?: (ctx: any, opts?: any) => Promise<void> | void };
51
+ if (typeof resolve === 'string') {
52
+ // Resolve from the user's project directory so the package is found
53
+ // in their node_modules, not in the CLI's own node_modules.
54
+ const projectRequire = createRequire(process.cwd() + '/package.json');
55
+
56
+ // Gatsby plugins export sourceNodes from gatsby-node.js, not their
57
+ // package main. Try gatsby-node.js first, fall back to main entry.
58
+ let pkgPath: string;
59
+ try {
60
+ pkgPath = projectRequire.resolve(`${resolve}/gatsby-node`);
61
+ } catch {
62
+ pkgPath = projectRequire.resolve(resolve);
63
+ }
64
+ mod = await import(pkgPath);
65
+ } else {
66
+ mod = resolve as any;
67
+ }
68
+
69
+ if (typeof mod.sourceNodes !== 'function') {
70
+ const name = typeof resolve === 'string' ? resolve : '(module)';
71
+ console.warn(`[hadars] source plugin ${name} does not export sourceNodes — skipping`);
72
+ continue;
73
+ }
74
+
75
+ const pluginName = typeof resolve === 'string' ? resolve : 'hadars-source';
76
+
77
+ // Track the last time createNode was called so we can detect settling.
78
+ let lastNodeTime = Date.now();
79
+ const trackingStore = new Proxy(store, {
80
+ get(target, prop) {
81
+ if (prop === 'createNode') {
82
+ return (node: any) => {
83
+ lastNodeTime = Date.now();
84
+ return target.createNode(node);
85
+ };
86
+ }
87
+ return (target as any)[prop];
88
+ },
89
+ });
90
+
91
+ // Real EventEmitter — plugins subscribe to BOOTSTRAP_FINISHED on this.
92
+ const emitter = new EventEmitter();
93
+ const ctx = makeGatsbyContext(trackingStore, pluginName, options, emitter);
94
+
95
+ try {
96
+ await mod.sourceNodes(ctx, options);
97
+ } catch (err) {
98
+ throw new Error(
99
+ `[hadars] source plugin "${pluginName}" threw during sourceNodes: ${(err as Error).message}`,
100
+ );
101
+ }
102
+
103
+ // Phase 1: wait for async initial-scan nodes (e.g. chokidar ready → createFileNode).
104
+ await waitForSettle(() => lastNodeTime);
105
+
106
+ // Signal Gatsby lifecycle end — some plugins create additional nodes here.
107
+ emitter.emit('BOOTSTRAP_FINISHED');
108
+
109
+ // Phase 2: wait for any post-bootstrap nodes to settle.
110
+ await waitForSettle(() => lastNodeTime);
111
+ }
112
+
113
+ return store;
114
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * In-memory node store — the equivalent of Gatsby's internal node database.
3
+ * Source plugins write nodes here via the context shim; the schema inferencer
4
+ * reads them to build the GraphQL schema.
5
+ */
6
+
7
+ export interface HadarsNode {
8
+ id: string;
9
+ /** Gatsby convention: the node type string, e.g. "MarkdownRemark", "ContentfulBlogPost". */
10
+ internal: {
11
+ type: string;
12
+ contentDigest: string;
13
+ content?: string;
14
+ mediaType?: string;
15
+ description?: string;
16
+ };
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ export class NodeStore {
21
+ private byId = new Map<string, HadarsNode>();
22
+ private byType = new Map<string, HadarsNode[]>();
23
+
24
+ createNode(node: HadarsNode): void {
25
+ this.byId.set(node.id, node);
26
+ const list = this.byType.get(node.internal.type) ?? [];
27
+ // Replace existing node with same id if present
28
+ const idx = list.findIndex(n => n.id === node.id);
29
+ if (idx >= 0) list[idx] = node; else list.push(node);
30
+ this.byType.set(node.internal.type, list);
31
+ }
32
+
33
+ getNode(id: string): HadarsNode | undefined {
34
+ return this.byId.get(id);
35
+ }
36
+
37
+ getNodes(): HadarsNode[] {
38
+ return Array.from(this.byId.values());
39
+ }
40
+
41
+ getNodesByType(type: string): HadarsNode[] {
42
+ return this.byType.get(type) ?? [];
43
+ }
44
+
45
+ getTypes(): string[] {
46
+ return Array.from(this.byType.keys());
47
+ }
48
+ }
package/src/static.ts ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Static site export core — rendering and file I/O.
3
+ *
4
+ * Imported by cli-lib.ts (bundled into dist/cli.js via esbuild) and by tests.
5
+ * Has no dependency on the rspack build pipeline — only SSR utilities.
6
+ */
7
+
8
+ import { cp, mkdir, writeFile } from 'node:fs/promises';
9
+ import { join, dirname, basename } from 'node:path';
10
+ import { parseRequest } from './utils/request';
11
+ import { getReactResponse, buildHeadHtml } from './utils/response';
12
+ import { buildSsrHtml, makePrecontentHtmlGetter } from './utils/ssrHandler';
13
+ import type { HadarsEntryModule, HadarsStaticContext, GraphQLExecutor } from './types/hadars';
14
+
15
+ export interface StaticRenderResult {
16
+ /** URL paths that were successfully rendered. */
17
+ rendered: string[];
18
+ /** Paths that failed, with the caught error. */
19
+ errors: Array<{ path: string; error: Error }>;
20
+ }
21
+
22
+ /**
23
+ * Pre-renders a list of URL paths to HTML files and copies static assets.
24
+ *
25
+ * Pages are rendered serially — `getReactResponse` writes to
26
+ * `globalThis.__hadarsUnsuspend / __hadarsContext` which are not re-entrant-safe.
27
+ */
28
+ export async function renderStaticSite(opts: {
29
+ ssrModule: HadarsEntryModule<any>;
30
+ htmlSource: string;
31
+ staticSrc: string;
32
+ paths: string[];
33
+ outputDir: string;
34
+ graphql?: GraphQLExecutor;
35
+ }): Promise<StaticRenderResult> {
36
+ const { ssrModule, htmlSource, staticSrc, paths, outputDir } = opts;
37
+
38
+ const staticCtx: HadarsStaticContext = {
39
+ graphql: opts.graphql ?? (() => Promise.reject(
40
+ new Error('[hadars] No graphql executor configured. Add a `graphql` function to your hadars.config.'),
41
+ )),
42
+ };
43
+ const getPrecontentHtml = makePrecontentHtmlGetter(Promise.resolve(htmlSource));
44
+
45
+ await mkdir(outputDir, { recursive: true });
46
+
47
+ const rendered: string[] = [];
48
+ const errors: Array<{ path: string; error: Error }> = [];
49
+
50
+ for (const urlPath of paths) {
51
+ try {
52
+ const req = parseRequest(new Request('http://localhost' + urlPath));
53
+
54
+ const { head, getAppBody, finalize } = await getReactResponse(req, {
55
+ document: {
56
+ body: ssrModule.default as any,
57
+ getInitProps: ssrModule.getInitProps,
58
+ getFinalProps: ssrModule.getFinalProps,
59
+ },
60
+ staticCtx,
61
+ });
62
+
63
+ const bodyHtml = await getAppBody();
64
+ const { clientProps } = await finalize();
65
+ const headHtml = buildHeadHtml(head);
66
+ // Inject a flag so the client knows it's a static export and should
67
+ // fetch index.json sidecars directly instead of hitting a live server.
68
+ const staticClientProps = { ...clientProps, __hadarsStatic: true };
69
+ const html = await buildSsrHtml(bodyHtml, staticClientProps, headHtml, getPrecontentHtml);
70
+
71
+ // '/' → <outputDir>/index.html
72
+ // '/about' → <outputDir>/about/index.html
73
+ const cleanPath = urlPath.replace(/\/$/, '');
74
+ const pageDir = cleanPath === '' ? outputDir : join(outputDir, cleanPath);
75
+ await mkdir(pageDir, { recursive: true });
76
+ await writeFile(join(pageDir, 'index.html'), html, 'utf-8');
77
+
78
+ // Write a JSON sidecar so useServerData can hydrate on client-side
79
+ // navigation without a live server. The format matches the live
80
+ // server's Accept: application/json response: { serverData: {...} }.
81
+ const serverData = (staticClientProps as any).__serverData ?? {};
82
+ await writeFile(
83
+ join(pageDir, 'index.json'),
84
+ JSON.stringify({ serverData }),
85
+ 'utf-8',
86
+ );
87
+
88
+ rendered.push(urlPath);
89
+ } catch (err: any) {
90
+ errors.push({
91
+ path: urlPath,
92
+ error: err instanceof Error ? err : new Error(String(err)),
93
+ });
94
+ }
95
+ }
96
+
97
+ // Copy .hadars/static/ → <outputDir>/static/, excluding the SSR template.
98
+ const staticDest = join(outputDir, 'static');
99
+ await mkdir(staticDest, { recursive: true });
100
+ await cp(staticSrc, staticDest, {
101
+ recursive: true,
102
+ filter: (src: string) => basename(src) !== 'out.html',
103
+ });
104
+
105
+ return { rendered, errors };
106
+ }
@@ -1,6 +1,35 @@
1
1
  import type { LinkHTMLAttributes, MetaHTMLAttributes, ScriptHTMLAttributes, StyleHTMLAttributes } from "react";
2
2
 
3
- export type HadarsGetInitialProps<T extends {}> = (req: HadarsRequest) => Promise<T> | T;
3
+ /**
4
+ * In-process GraphQL executor passed to `getInitProps` and `paths` during
5
+ * `hadars export static`. Hadars is executor-agnostic — configure it in
6
+ * `hadars.config.ts` using any GraphQL library (e.g. `graphql-js`):
7
+ *
8
+ * ```ts
9
+ * import { graphql as gql, buildSchema } from 'graphql';
10
+ * const schema = buildSchema(`type Query { hello: String }`);
11
+ * const rootValue = { hello: () => 'world' };
12
+ *
13
+ * export default {
14
+ * graphql: (query, variables) =>
15
+ * gql({ schema, rootValue, source: query, variableValues: variables }),
16
+ * } satisfies HadarsOptions;
17
+ * ```
18
+ */
19
+ export type GraphQLExecutor = (
20
+ query: string,
21
+ variables?: Record<string, unknown>,
22
+ ) => Promise<{ data?: any; errors?: ReadonlyArray<{ message: string }> }>;
23
+
24
+ /**
25
+ * Context passed as the second argument to `getInitProps` and `paths`
26
+ * during `hadars export static`. Not present in dev/run mode.
27
+ */
28
+ export interface HadarsStaticContext {
29
+ graphql: GraphQLExecutor;
30
+ }
31
+
32
+ export type HadarsGetInitialProps<T extends {}> = (req: HadarsRequest, ctx?: HadarsStaticContext) => Promise<T> | T;
4
33
  export type HadarsGetClientProps<T extends {}> = (props: T) => Promise<T> | T;
5
34
  export type HadarsGetFinalProps<T extends {}> = (props: HadarsProps<T>) => Promise<T> | T;
6
35
  export type HadarsApp<T extends {}> = React.FC<HadarsProps<T>>;
@@ -180,6 +209,67 @@ export interface HadarsOptions {
180
209
  */
181
210
  cache?: (req: HadarsRequest) => { key: string; ttl?: number } | null | undefined
182
211
  | Promise<{ key: string; ttl?: number } | null | undefined>;
212
+ /**
213
+ * Static export path list. Required for `hadars export static`.
214
+ *
215
+ * Return an array of URL paths (e.g. `['/', '/about', '/blog/hello']`) that
216
+ * should be pre-rendered to HTML files. May be async.
217
+ *
218
+ * @example
219
+ * paths: () => ['/', '/about', '/contact']
220
+ *
221
+ * @example
222
+ * paths: async () => {
223
+ * const posts = await fetchBlogPosts();
224
+ * return ['/', ...posts.map(p => `/blog/${p.slug}`)];
225
+ * }
226
+ */
227
+ /**
228
+ * In-process GraphQL executor. Supply this to use the GraphQL data layer
229
+ * in `paths` and `getInitProps` during `hadars export static`.
230
+ * Has no effect in `dev` / `run` mode.
231
+ */
232
+ graphql?: GraphQLExecutor;
233
+ paths?: (ctx: HadarsStaticContext) => Promise<string[]> | string[];
234
+ /**
235
+ * Gatsby-compatible source plugins to run before `hadars export static`.
236
+ *
237
+ * Each entry mirrors Gatsby's `gatsby-config.js` plugin object format:
238
+ * `{ resolve: 'gatsby-source-contentful', options: { spaceId: '...', accessToken: '...' } }`
239
+ *
240
+ * The plugin must export a `sourceNodes` function with the standard Gatsby API.
241
+ * Hadars provides a thin shim covering the most-used surface:
242
+ * `actions.createNode`, `createNodeId`, `createContentDigest`, `cache`, `reporter`,
243
+ * `getNode`, `getNodes`, `getNodesByType`.
244
+ *
245
+ * After all sources have run, hadars auto-generates a GraphQL schema from the
246
+ * collected nodes and makes it available via `config.graphql` in `getInitProps`
247
+ * and `paths`. Requires `graphql` to be installed in the project.
248
+ *
249
+ * @example
250
+ * sources: [
251
+ * {
252
+ * resolve: 'gatsby-source-filesystem',
253
+ * options: { name: 'posts', path: './content/posts' },
254
+ * },
255
+ * ]
256
+ */
257
+ sources?: HadarsSourceEntry[];
258
+ }
259
+
260
+ /**
261
+ * A Gatsby-compatible source plugin entry, matching the format used in
262
+ * `gatsby-config.js` / `gatsby-config.ts`.
263
+ */
264
+ export interface HadarsSourceEntry {
265
+ /**
266
+ * Package name (e.g. `'gatsby-source-contentful'`) or a pre-imported module
267
+ * object that exports `sourceNodes`. Using a module object lets you pass
268
+ * local source plugins without publishing them to npm.
269
+ */
270
+ resolve: string | { sourceNodes?: (ctx: any, opts?: any) => Promise<void> | void };
271
+ /** Plugin options forwarded as the second argument to `sourceNodes`. */
272
+ options?: Record<string, unknown>;
183
273
  }
184
274
 
185
275