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.
@@ -1,4 +1,33 @@
1
- type HadarsGetInitialProps<T extends {}> = (req: HadarsRequest) => Promise<T> | T;
1
+ /**
2
+ * In-process GraphQL executor passed to `getInitProps` and `paths` during
3
+ * `hadars export static`. Hadars is executor-agnostic — configure it in
4
+ * `hadars.config.ts` using any GraphQL library (e.g. `graphql-js`):
5
+ *
6
+ * ```ts
7
+ * import { graphql as gql, buildSchema } from 'graphql';
8
+ * const schema = buildSchema(`type Query { hello: String }`);
9
+ * const rootValue = { hello: () => 'world' };
10
+ *
11
+ * export default {
12
+ * graphql: (query, variables) =>
13
+ * gql({ schema, rootValue, source: query, variableValues: variables }),
14
+ * } satisfies HadarsOptions;
15
+ * ```
16
+ */
17
+ type GraphQLExecutor = (query: string, variables?: Record<string, unknown>) => Promise<{
18
+ data?: any;
19
+ errors?: ReadonlyArray<{
20
+ message: string;
21
+ }>;
22
+ }>;
23
+ /**
24
+ * Context passed as the second argument to `getInitProps` and `paths`
25
+ * during `hadars export static`. Not present in dev/run mode.
26
+ */
27
+ interface HadarsStaticContext {
28
+ graphql: GraphQLExecutor;
29
+ }
30
+ type HadarsGetInitialProps<T extends {}> = (req: HadarsRequest, ctx?: HadarsStaticContext) => Promise<T> | T;
2
31
  type HadarsGetClientProps<T extends {}> = (props: T) => Promise<T> | T;
3
32
  type HadarsGetFinalProps<T extends {}> = (props: HadarsProps<T>) => Promise<T> | T;
4
33
  type HadarsApp<T extends {}> = React.FC<HadarsProps<T>>;
@@ -148,6 +177,68 @@ interface HadarsOptions {
148
177
  key: string;
149
178
  ttl?: number;
150
179
  } | null | undefined>;
180
+ /**
181
+ * Static export path list. Required for `hadars export static`.
182
+ *
183
+ * Return an array of URL paths (e.g. `['/', '/about', '/blog/hello']`) that
184
+ * should be pre-rendered to HTML files. May be async.
185
+ *
186
+ * @example
187
+ * paths: () => ['/', '/about', '/contact']
188
+ *
189
+ * @example
190
+ * paths: async () => {
191
+ * const posts = await fetchBlogPosts();
192
+ * return ['/', ...posts.map(p => `/blog/${p.slug}`)];
193
+ * }
194
+ */
195
+ /**
196
+ * In-process GraphQL executor. Supply this to use the GraphQL data layer
197
+ * in `paths` and `getInitProps` during `hadars export static`.
198
+ * Has no effect in `dev` / `run` mode.
199
+ */
200
+ graphql?: GraphQLExecutor;
201
+ paths?: (ctx: HadarsStaticContext) => Promise<string[]> | string[];
202
+ /**
203
+ * Gatsby-compatible source plugins to run before `hadars export static`.
204
+ *
205
+ * Each entry mirrors Gatsby's `gatsby-config.js` plugin object format:
206
+ * `{ resolve: 'gatsby-source-contentful', options: { spaceId: '...', accessToken: '...' } }`
207
+ *
208
+ * The plugin must export a `sourceNodes` function with the standard Gatsby API.
209
+ * Hadars provides a thin shim covering the most-used surface:
210
+ * `actions.createNode`, `createNodeId`, `createContentDigest`, `cache`, `reporter`,
211
+ * `getNode`, `getNodes`, `getNodesByType`.
212
+ *
213
+ * After all sources have run, hadars auto-generates a GraphQL schema from the
214
+ * collected nodes and makes it available via `config.graphql` in `getInitProps`
215
+ * and `paths`. Requires `graphql` to be installed in the project.
216
+ *
217
+ * @example
218
+ * sources: [
219
+ * {
220
+ * resolve: 'gatsby-source-filesystem',
221
+ * options: { name: 'posts', path: './content/posts' },
222
+ * },
223
+ * ]
224
+ */
225
+ sources?: HadarsSourceEntry[];
226
+ }
227
+ /**
228
+ * A Gatsby-compatible source plugin entry, matching the format used in
229
+ * `gatsby-config.js` / `gatsby-config.ts`.
230
+ */
231
+ interface HadarsSourceEntry {
232
+ /**
233
+ * Package name (e.g. `'gatsby-source-contentful'`) or a pre-imported module
234
+ * object that exports `sourceNodes`. Using a module object lets you pass
235
+ * local source plugins without publishing them to npm.
236
+ */
237
+ resolve: string | {
238
+ sourceNodes?: (ctx: any, opts?: any) => Promise<void> | void;
239
+ };
240
+ /** Plugin options forwarded as the second argument to `sourceNodes`. */
241
+ options?: Record<string, unknown>;
151
242
  }
152
243
  type SwcPluginItem = string | [string, Record<string, unknown>] | {
153
244
  path: string;
@@ -161,4 +252,4 @@ interface HadarsRequest extends Request {
161
252
  cookies: Record<string, string>;
162
253
  }
163
254
 
164
- export type { HadarsEntryModule as H, HadarsOptions as a, HadarsApp as b, HadarsGetClientProps as c, HadarsGetFinalProps as d, HadarsGetInitialProps as e, HadarsProps as f, HadarsRequest as g };
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 };
@@ -1,4 +1,33 @@
1
- type HadarsGetInitialProps<T extends {}> = (req: HadarsRequest) => Promise<T> | T;
1
+ /**
2
+ * In-process GraphQL executor passed to `getInitProps` and `paths` during
3
+ * `hadars export static`. Hadars is executor-agnostic — configure it in
4
+ * `hadars.config.ts` using any GraphQL library (e.g. `graphql-js`):
5
+ *
6
+ * ```ts
7
+ * import { graphql as gql, buildSchema } from 'graphql';
8
+ * const schema = buildSchema(`type Query { hello: String }`);
9
+ * const rootValue = { hello: () => 'world' };
10
+ *
11
+ * export default {
12
+ * graphql: (query, variables) =>
13
+ * gql({ schema, rootValue, source: query, variableValues: variables }),
14
+ * } satisfies HadarsOptions;
15
+ * ```
16
+ */
17
+ type GraphQLExecutor = (query: string, variables?: Record<string, unknown>) => Promise<{
18
+ data?: any;
19
+ errors?: ReadonlyArray<{
20
+ message: string;
21
+ }>;
22
+ }>;
23
+ /**
24
+ * Context passed as the second argument to `getInitProps` and `paths`
25
+ * during `hadars export static`. Not present in dev/run mode.
26
+ */
27
+ interface HadarsStaticContext {
28
+ graphql: GraphQLExecutor;
29
+ }
30
+ type HadarsGetInitialProps<T extends {}> = (req: HadarsRequest, ctx?: HadarsStaticContext) => Promise<T> | T;
2
31
  type HadarsGetClientProps<T extends {}> = (props: T) => Promise<T> | T;
3
32
  type HadarsGetFinalProps<T extends {}> = (props: HadarsProps<T>) => Promise<T> | T;
4
33
  type HadarsApp<T extends {}> = React.FC<HadarsProps<T>>;
@@ -148,6 +177,68 @@ interface HadarsOptions {
148
177
  key: string;
149
178
  ttl?: number;
150
179
  } | null | undefined>;
180
+ /**
181
+ * Static export path list. Required for `hadars export static`.
182
+ *
183
+ * Return an array of URL paths (e.g. `['/', '/about', '/blog/hello']`) that
184
+ * should be pre-rendered to HTML files. May be async.
185
+ *
186
+ * @example
187
+ * paths: () => ['/', '/about', '/contact']
188
+ *
189
+ * @example
190
+ * paths: async () => {
191
+ * const posts = await fetchBlogPosts();
192
+ * return ['/', ...posts.map(p => `/blog/${p.slug}`)];
193
+ * }
194
+ */
195
+ /**
196
+ * In-process GraphQL executor. Supply this to use the GraphQL data layer
197
+ * in `paths` and `getInitProps` during `hadars export static`.
198
+ * Has no effect in `dev` / `run` mode.
199
+ */
200
+ graphql?: GraphQLExecutor;
201
+ paths?: (ctx: HadarsStaticContext) => Promise<string[]> | string[];
202
+ /**
203
+ * Gatsby-compatible source plugins to run before `hadars export static`.
204
+ *
205
+ * Each entry mirrors Gatsby's `gatsby-config.js` plugin object format:
206
+ * `{ resolve: 'gatsby-source-contentful', options: { spaceId: '...', accessToken: '...' } }`
207
+ *
208
+ * The plugin must export a `sourceNodes` function with the standard Gatsby API.
209
+ * Hadars provides a thin shim covering the most-used surface:
210
+ * `actions.createNode`, `createNodeId`, `createContentDigest`, `cache`, `reporter`,
211
+ * `getNode`, `getNodes`, `getNodesByType`.
212
+ *
213
+ * After all sources have run, hadars auto-generates a GraphQL schema from the
214
+ * collected nodes and makes it available via `config.graphql` in `getInitProps`
215
+ * and `paths`. Requires `graphql` to be installed in the project.
216
+ *
217
+ * @example
218
+ * sources: [
219
+ * {
220
+ * resolve: 'gatsby-source-filesystem',
221
+ * options: { name: 'posts', path: './content/posts' },
222
+ * },
223
+ * ]
224
+ */
225
+ sources?: HadarsSourceEntry[];
226
+ }
227
+ /**
228
+ * A Gatsby-compatible source plugin entry, matching the format used in
229
+ * `gatsby-config.js` / `gatsby-config.ts`.
230
+ */
231
+ interface HadarsSourceEntry {
232
+ /**
233
+ * Package name (e.g. `'gatsby-source-contentful'`) or a pre-imported module
234
+ * object that exports `sourceNodes`. Using a module object lets you pass
235
+ * local source plugins without publishing them to npm.
236
+ */
237
+ resolve: string | {
238
+ sourceNodes?: (ctx: any, opts?: any) => Promise<void> | void;
239
+ };
240
+ /** Plugin options forwarded as the second argument to `sourceNodes`. */
241
+ options?: Record<string, unknown>;
151
242
  }
152
243
  type SwcPluginItem = string | [string, Record<string, unknown>] | {
153
244
  path: string;
@@ -161,4 +252,4 @@ interface HadarsRequest extends Request {
161
252
  cookies: Record<string, string>;
162
253
  }
163
254
 
164
- export type { HadarsEntryModule as H, HadarsOptions as a, HadarsApp as b, HadarsGetClientProps as c, HadarsGetFinalProps as d, HadarsGetInitialProps as e, HadarsProps as f, HadarsRequest as g };
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 };
package/dist/index.cjs CHANGED
@@ -226,14 +226,17 @@ function useServerData(key, fn) {
226
226
  pendingDataFetch.set(pathKey, p);
227
227
  queueMicrotask(async () => {
228
228
  try {
229
- const res = await fetch(pathKey, {
230
- headers: { "Accept": "application/json" }
231
- });
232
- if (res.ok) {
233
- const json = await res.json();
234
- for (const [k, v] of Object.entries(json.serverData ?? {})) {
235
- clientServerDataCache.set(k, v);
236
- }
229
+ let json = null;
230
+ if (globalThis.__hadarsStatic) {
231
+ const sidecarUrl = pathKey.replace(/\/$/, "") + "/index.json";
232
+ const res = await fetch(sidecarUrl).catch(() => null);
233
+ if (res?.ok) json = await res.json().catch(() => null);
234
+ } else {
235
+ const res = await fetch(pathKey, { headers: { "Accept": "application/json" } });
236
+ if (res.ok) json = await res.json();
237
+ }
238
+ for (const [k, v] of Object.entries(json?.serverData ?? {})) {
239
+ clientServerDataCache.set(k, v);
237
240
  }
238
241
  } finally {
239
242
  fetchedPaths.add(pathKey);
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { b as HadarsApp, H as HadarsEntryModule, c as HadarsGetClientProps, d as HadarsGetFinalProps, e as HadarsGetInitialProps, a as HadarsOptions, f as HadarsProps, g as HadarsRequest } from './hadars-DEBSYAQl.cjs';
1
+ export { G as GraphQLExecutor, b as HadarsApp, H as HadarsEntryModule, c as HadarsGetClientProps, d as HadarsGetFinalProps, e as HadarsGetInitialProps, a as HadarsOptions, f as HadarsProps, g as HadarsRequest, h as HadarsSourceEntry, i as HadarsStaticContext } from './hadars-mKu5txjW.cjs';
2
2
  import React from 'react';
3
3
 
4
4
  /** Call this before hydrating to seed the client cache from the server's data.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { b as HadarsApp, H as HadarsEntryModule, c as HadarsGetClientProps, d as HadarsGetFinalProps, e as HadarsGetInitialProps, a as HadarsOptions, f as HadarsProps, g as HadarsRequest } from './hadars-DEBSYAQl.js';
1
+ export { G as GraphQLExecutor, b as HadarsApp, H as HadarsEntryModule, c as HadarsGetClientProps, d as HadarsGetFinalProps, e as HadarsGetInitialProps, a as HadarsOptions, f as HadarsProps, g as HadarsRequest, h as HadarsSourceEntry, i as HadarsStaticContext } from './hadars-mKu5txjW.js';
2
2
  import React from 'react';
3
3
 
4
4
  /** Call this before hydrating to seed the client cache from the server's data.
package/dist/index.js CHANGED
@@ -187,14 +187,17 @@ function useServerData(key, fn) {
187
187
  pendingDataFetch.set(pathKey, p);
188
188
  queueMicrotask(async () => {
189
189
  try {
190
- const res = await fetch(pathKey, {
191
- headers: { "Accept": "application/json" }
192
- });
193
- if (res.ok) {
194
- const json = await res.json();
195
- for (const [k, v] of Object.entries(json.serverData ?? {})) {
196
- clientServerDataCache.set(k, v);
197
- }
190
+ let json = null;
191
+ if (globalThis.__hadarsStatic) {
192
+ const sidecarUrl = pathKey.replace(/\/$/, "") + "/index.json";
193
+ const res = await fetch(sidecarUrl).catch(() => null);
194
+ if (res?.ok) json = await res.json().catch(() => null);
195
+ } else {
196
+ const res = await fetch(pathKey, { headers: { "Accept": "application/json" } });
197
+ if (res.ok) json = await res.json();
198
+ }
199
+ for (const [k, v] of Object.entries(json?.serverData ?? {})) {
200
+ clientServerDataCache.set(k, v);
198
201
  }
199
202
  } finally {
200
203
  fetchedPaths.add(pathKey);
package/dist/lambda.cjs CHANGED
@@ -1226,7 +1226,7 @@ var getReactResponse = async (req, opts) => {
1226
1226
  head: { title: "Hadars App", meta: {}, link: {}, style: {}, script: {}, status: 200 }
1227
1227
  };
1228
1228
  let props = {
1229
- ...getInitProps ? await getInitProps(req) : {},
1229
+ ...getInitProps ? await getInitProps(req, opts.staticCtx) : {},
1230
1230
  location: req.location,
1231
1231
  context
1232
1232
  };
@@ -1252,7 +1252,7 @@ var getReactResponse = async (req, opts) => {
1252
1252
  }
1253
1253
  };
1254
1254
  const finalize = async () => {
1255
- const { context: _, ...restProps } = getFinalProps ? await getFinalProps(props) : props;
1255
+ const restProps = getFinalProps ? await getFinalProps(props) : props;
1256
1256
  const serverData = {};
1257
1257
  let hasServerData = false;
1258
1258
  for (const [key, entry] of unsuspend.cache) {
package/dist/lambda.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { H as HadarsEntryModule, a as HadarsOptions } from './hadars-DEBSYAQl.cjs';
1
+ import { H as HadarsEntryModule, a as HadarsOptions } from './hadars-mKu5txjW.cjs';
2
2
 
3
3
  /**
4
4
  * AWS Lambda adapter for hadars.
package/dist/lambda.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { H as HadarsEntryModule, a as HadarsOptions } from './hadars-DEBSYAQl.js';
1
+ import { H as HadarsEntryModule, a as HadarsOptions } from './hadars-mKu5txjW.js';
2
2
 
3
3
  /**
4
4
  * AWS Lambda adapter for hadars.
package/dist/lambda.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  getReactResponse,
7
7
  makePrecontentHtmlGetter,
8
8
  parseRequest
9
- } from "./chunk-HWOLYLPF.js";
9
+ } from "./chunk-H72BZXOA.js";
10
10
  import "./chunk-LY5MTHFV.js";
11
11
  import "./chunk-OS3V4CPN.js";
12
12
 
@@ -246,14 +246,23 @@ export function useServerData<T>(key: string | string[], fn: () => Promise<T> |
246
246
  // render is registered against the same deferred before the fetch starts.
247
247
  queueMicrotask(async () => {
248
248
  try {
249
- const res = await fetch(pathKey, {
250
- headers: { 'Accept': 'application/json' },
251
- });
252
- if (res.ok) {
253
- const json = await res.json() as { serverData: Record<string, unknown> };
254
- for (const [k, v] of Object.entries(json.serverData ?? {})) {
255
- clientServerDataCache.set(k, v);
256
- }
249
+ let json: { serverData: Record<string, unknown> } | null = null;
250
+
251
+ if ((globalThis as any).__hadarsStatic) {
252
+ // Static export: the __hadarsStatic flag was embedded in the
253
+ // page by `hadars export static`. Fetch the pre-generated
254
+ // index.json sidecar directly no live SSR server exists.
255
+ const sidecarUrl = pathKey.replace(/\/$/, '') + '/index.json';
256
+ const res = await fetch(sidecarUrl).catch(() => null);
257
+ if (res?.ok) json = await res.json().catch(() => null);
258
+ } else {
259
+ // Live server: request the current URL with Accept: application/json.
260
+ const res = await fetch(pathKey, { headers: { 'Accept': 'application/json' } });
261
+ if (res.ok) json = await res.json();
262
+ }
263
+
264
+ for (const [k, v] of Object.entries(json?.serverData ?? {})) {
265
+ clientServerDataCache.set(k, v);
257
266
  }
258
267
  } finally {
259
268
  fetchedPaths.add(pathKey);
@@ -22,6 +22,15 @@ const getProps = () => {
22
22
  const main = async () => {
23
23
  let props = getProps();
24
24
 
25
+ // Extract the static-export flag before it reaches user code. When set,
26
+ // useServerData fetches index.json sidecars directly on client navigation
27
+ // instead of requesting the live SSR server with Accept: application/json.
28
+ if ((props as any).__hadarsStatic) {
29
+ (globalThis as any).__hadarsStatic = true;
30
+ const { __hadarsStatic: _, ...rest } = props as any;
31
+ props = rest;
32
+ }
33
+
25
34
  // Seed the useServerData client cache from server-resolved values before
26
35
  // hydration so that hooks return the same data on the first render.
27
36
  if (props.__serverData && typeof props.__serverData === 'object') {
package/index.ts CHANGED
@@ -10,5 +10,8 @@ export type {
10
10
  HadarsGetClientProps,
11
11
  HadarsEntryModule,
12
12
  HadarsApp,
13
+ HadarsStaticContext,
14
+ GraphQLExecutor,
15
+ HadarsSourceEntry,
13
16
  } from "./src/types/hadars";
14
17
  export { HadarsHead, HadarsContext, loadModule } from "./src/index";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hadars",
3
- "version": "0.2.1",
3
+ "version": "0.2.2-rc.0",
4
4
  "description": "Minimal SSR framework for React — rspack, HMR, TypeScript, Bun/Node/Deno",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
package/src/build.ts CHANGED
@@ -20,6 +20,9 @@ import {
20
20
  buildSsrResponse, makePrecontentHtmlGetter,
21
21
  type CacheFetchHandler, createRenderCache,
22
22
  } from './utils/ssrHandler';
23
+ import { runSources } from './source/runner';
24
+ import { buildSchemaExecutor } from './source/inference';
25
+ import { createGraphiqlHandler, GRAPHQL_PATH } from './source/graphiql';
23
26
 
24
27
  /**
25
28
  * Reads an HTML template, processes any `<style>` blocks through PostCSS
@@ -295,6 +298,26 @@ export const dev = async (options: HadarsRuntimeOptions) => {
295
298
  const handleWS = upgradeHandler(options);
296
299
  const handler = options.fetch;
297
300
 
301
+ // Run source plugins and set up GraphiQL if config.sources is present.
302
+ let handleGraphiql: ((req: Request) => Promise<Response | undefined>) | null = null;
303
+ let devStaticCtx: { graphql: import('./types/hadars').GraphQLExecutor } | undefined;
304
+ if (options.sources && options.sources.length > 0) {
305
+ console.log(`[hadars] Running ${options.sources.length} source plugin(s)…`);
306
+ try {
307
+ const store = await runSources(options.sources);
308
+ const executor = await buildSchemaExecutor(store);
309
+ if (executor) {
310
+ devStaticCtx = { graphql: executor };
311
+ handleGraphiql = createGraphiqlHandler(executor);
312
+ console.log(`[hadars] GraphiQL available at http://localhost:${port}${GRAPHQL_PATH}`);
313
+ } else {
314
+ console.warn('[hadars] `graphql` package not found — GraphiQL disabled. Run: npm install graphql');
315
+ }
316
+ } catch (err) {
317
+ console.error('[hadars] Source plugin error:', err);
318
+ }
319
+ }
320
+
298
321
  const entry = pathMod.resolve(__dirname, options.entry);
299
322
  const hmrPort = options.hmrPort ?? port + 1;
300
323
 
@@ -491,6 +514,11 @@ export const dev = async (options: HadarsRuntimeOptions) => {
491
514
  }
492
515
  if (handleWS && handleWS(request, ctx)) return undefined;
493
516
 
517
+ if (handleGraphiql) {
518
+ const graphiqlRes = await handleGraphiql(req);
519
+ if (graphiqlRes) return graphiqlRes;
520
+ }
521
+
494
522
  const proxied = await handleProxy(request);
495
523
  if (proxied) return proxied;
496
524
 
@@ -525,6 +553,7 @@ export const dev = async (options: HadarsRuntimeOptions) => {
525
553
  getInitProps,
526
554
  getFinalProps,
527
555
  },
556
+ staticCtx: devStaticCtx,
528
557
  });
529
558
 
530
559
  // Content negotiation: if the client only accepts JSON (client-side
package/src/index.tsx CHANGED
@@ -7,6 +7,9 @@ export type {
7
7
  HadarsGetClientProps,
8
8
  HadarsEntryModule,
9
9
  HadarsApp,
10
+ HadarsStaticContext,
11
+ GraphQLExecutor,
12
+ HadarsSourceEntry,
10
13
  } from "./types/hadars";
11
14
  export { Head as HadarsHead, useServerData, initServerDataCache } from './utils/Head';
12
15
 
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Gatsby sourceNodes context shim.
3
+ *
4
+ * Gatsby passes a rich object to each source plugin's `sourceNodes` function.
5
+ * We implement the subset that the vast majority of CMS source plugins actually
6
+ * use so that existing plugins work without modification.
7
+ */
8
+
9
+ import { createHash } from 'node:crypto';
10
+ import { EventEmitter } from 'node:events';
11
+ import type { NodeStore, HadarsNode } from './store';
12
+
13
+ // ── Types ─────────────────────────────────────────────────────────────────────
14
+
15
+ export interface GatsbyCache {
16
+ get(key: string): Promise<unknown>;
17
+ set(key: string, value: unknown): Promise<void>;
18
+ }
19
+
20
+ export interface GatsbyReporter {
21
+ info(message: string): void;
22
+ warn(message: string): void;
23
+ error(message: string, err?: Error): void;
24
+ panic(message: string, err?: Error): never;
25
+ activityTimer(name: string): { start(): void; end(): void };
26
+ verbose(message: string): void;
27
+ }
28
+
29
+ export interface GatsbyActions {
30
+ createNode(node: HadarsNode): void;
31
+ deleteNode(node: { id: string }): void;
32
+ touchNode(node: { id: string }): void;
33
+ }
34
+
35
+ export interface GatsbyNodeHelpers {
36
+ actions: GatsbyActions;
37
+ createNodeId(input: string): string;
38
+ createContentDigest(content: unknown): string;
39
+ getNode(id: string): HadarsNode | undefined;
40
+ getNodes(): HadarsNode[];
41
+ getNodesByType(type: string): HadarsNode[];
42
+ cache: GatsbyCache;
43
+ reporter: GatsbyReporter;
44
+ // Gatsby passes these; many plugins destructure but never call them
45
+ store: unknown;
46
+ emitter: unknown;
47
+ tracing: unknown;
48
+ schema: unknown;
49
+ /** Available in Gatsby 4+ — mostly unused by source plugins. */
50
+ parentSpan: unknown;
51
+ }
52
+
53
+ // ── Factory ───────────────────────────────────────────────────────────────────
54
+
55
+ export function makeGatsbyContext(
56
+ store: NodeStore,
57
+ pluginName: string,
58
+ pluginOptions: Record<string, unknown> = {},
59
+ emitter: EventEmitter = new EventEmitter(),
60
+ ): GatsbyNodeHelpers {
61
+ // A simple in-memory cache per plugin instance
62
+ const cacheMap = new Map<string, unknown>();
63
+ const cache: GatsbyCache = {
64
+ get: (key) => Promise.resolve(cacheMap.get(key)),
65
+ set: (key, value) => { cacheMap.set(key, value); return Promise.resolve(); },
66
+ };
67
+
68
+ const reporter: GatsbyReporter = {
69
+ info: (msg) => console.log(`[${pluginName}] ${msg}`),
70
+ warn: (msg) => console.warn(`[${pluginName}] WARN: ${msg}`),
71
+ error: (msg, err) => console.error(`[${pluginName}] ERROR: ${msg}`, err ?? ''),
72
+ panic: (msg, err) => { console.error(`[${pluginName}] PANIC: ${msg}`, err ?? ''); process.exit(1); },
73
+ verbose: (msg) => { if (process.env.HADARS_VERBOSE) console.log(`[${pluginName}] ${msg}`); },
74
+ activityTimer: (name) => ({
75
+ start: () => { if (process.env.HADARS_VERBOSE) console.log(`[${pluginName}] ▶ ${name}`); },
76
+ end: () => { if (process.env.HADARS_VERBOSE) console.log(`[${pluginName}] ■ ${name}`); },
77
+ }),
78
+ // Gatsby 4+ reporter extras — used by some plugins but safe to no-op
79
+ setErrorMap: () => {},
80
+ log: (msg: string) => console.log(`[${pluginName}] ${msg}`),
81
+ success: (msg: string) => console.log(`[${pluginName}] ✓ ${msg}`),
82
+ } as any;
83
+
84
+ const actions: GatsbyActions = {
85
+ createNode: (node) => store.createNode(node),
86
+ deleteNode: ({ id }) => {
87
+ // NodeStore doesn't expose delete — it's rarely needed by source plugins
88
+ // on first run, so we silently no-op. A full implementation would add
89
+ // store.deleteNode().
90
+ },
91
+ touchNode: () => { /* no-op — only relevant for Gatsby's caching layer */ },
92
+ };
93
+
94
+ return {
95
+ actions,
96
+ createNodeId: (input) => createHash('sha256').update(pluginName + input).digest('hex'),
97
+ createContentDigest: (content) => {
98
+ const str = typeof content === 'string' ? content : JSON.stringify(content);
99
+ return createHash('md5').update(str).digest('hex');
100
+ },
101
+ getNode: (id) => store.getNode(id),
102
+ getNodes: () => store.getNodes(),
103
+ getNodesByType: (t) => store.getNodesByType(t),
104
+ cache,
105
+ reporter,
106
+ // Stubs for rarely-used Gatsby internals that plugins may destructure
107
+ store: {},
108
+ emitter,
109
+ tracing: { startSpan: () => ({ finish: () => {} }) },
110
+ schema: {},
111
+ parentSpan: null,
112
+ };
113
+ }