hadars 0.4.1 → 0.4.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.
Files changed (58) hide show
  1. package/dist/{chunk-TV37IMRB.js → chunk-2TMQUXFL.js} +10 -10
  2. package/dist/{chunk-2J2L2H3H.js → chunk-NYLXE7T7.js} +6 -6
  3. package/dist/{chunk-OS3V4CPN.js → chunk-OZUZS2PD.js} +4 -4
  4. package/dist/cli.js +462 -496
  5. package/dist/cloudflare.cjs +11 -11
  6. package/dist/cloudflare.js +3 -3
  7. package/dist/index.d.cts +8 -4
  8. package/dist/index.d.ts +8 -4
  9. package/dist/lambda.cjs +11 -11
  10. package/dist/lambda.js +7 -7
  11. package/dist/loader.cjs +90 -54
  12. package/dist/slim-react/index.cjs +13 -13
  13. package/dist/slim-react/index.js +2 -2
  14. package/dist/slim-react/jsx-runtime.cjs +2 -4
  15. package/dist/slim-react/jsx-runtime.js +1 -1
  16. package/dist/ssr-render-worker.js +174 -161
  17. package/dist/ssr-watch.js +40 -74
  18. package/package.json +8 -10
  19. package/cli-lib.ts +0 -676
  20. package/cli.ts +0 -36
  21. package/index.ts +0 -17
  22. package/src/build.ts +0 -805
  23. package/src/cloudflare.ts +0 -140
  24. package/src/index.tsx +0 -41
  25. package/src/lambda.ts +0 -287
  26. package/src/slim-react/context.ts +0 -55
  27. package/src/slim-react/dispatcher.ts +0 -87
  28. package/src/slim-react/hooks.ts +0 -137
  29. package/src/slim-react/index.ts +0 -232
  30. package/src/slim-react/jsx-runtime.ts +0 -7
  31. package/src/slim-react/jsx.ts +0 -53
  32. package/src/slim-react/render.ts +0 -1101
  33. package/src/slim-react/renderContext.ts +0 -294
  34. package/src/slim-react/types.ts +0 -33
  35. package/src/source/context.ts +0 -113
  36. package/src/source/graphiql.ts +0 -101
  37. package/src/source/inference.ts +0 -260
  38. package/src/source/runner.ts +0 -138
  39. package/src/source/store.ts +0 -50
  40. package/src/ssr-render-worker.ts +0 -116
  41. package/src/ssr-watch.ts +0 -62
  42. package/src/static.ts +0 -109
  43. package/src/types/global.d.ts +0 -5
  44. package/src/types/hadars.ts +0 -350
  45. package/src/utils/Head.tsx +0 -462
  46. package/src/utils/clientScript.tsx +0 -71
  47. package/src/utils/cookies.ts +0 -16
  48. package/src/utils/loader.ts +0 -335
  49. package/src/utils/proxyHandler.tsx +0 -104
  50. package/src/utils/request.tsx +0 -9
  51. package/src/utils/response.tsx +0 -141
  52. package/src/utils/rspack.ts +0 -467
  53. package/src/utils/runtime.ts +0 -19
  54. package/src/utils/serve.ts +0 -155
  55. package/src/utils/ssrHandler.ts +0 -239
  56. package/src/utils/staticFile.ts +0 -43
  57. package/src/utils/template.html +0 -11
  58. package/src/utils/upgradeRequest.tsx +0 -19
@@ -1,294 +0,0 @@
1
- /**
2
- * Render-time context for tree-position-based `useId` and React Context values.
3
- *
4
- * State lives on `globalThis` rather than module-level variables so that
5
- * multiple slim-react instances (the render worker's direct import and the
6
- * SSR bundle's bundled copy) share the same singletons without coordination.
7
- *
8
- * Context values are stored in a plain Map that is created per render call.
9
- * Node.js is single-threaded: only one render is executing between any two
10
- * await points. The renderer captures the map reference before every await
11
- * and restores it in the continuation, so concurrent renders stay isolated
12
- * without any external dependencies.
13
- */
14
-
15
- // The context map for the render that is currently executing (between awaits).
16
- // Kept on globalThis so both slim-react instances share the same slot.
17
- const MAP_KEY = "__slimReactContextMap";
18
- const _g = globalThis as any;
19
- if (!("__slimReactContextMap" in _g)) _g[MAP_KEY] = null;
20
-
21
- /**
22
- * Swap in a new context map and return the previous one.
23
- * Called at render entry-points and at every await/then continuation to
24
- * restore the correct map for the resuming render.
25
- */
26
- export function swapContextMap(
27
- map: Map<object, unknown> | null,
28
- ): Map<object, unknown> | null {
29
- const prev: Map<object, unknown> | null = _g[MAP_KEY];
30
- _g[MAP_KEY] = map;
31
- return prev;
32
- }
33
-
34
- /** Return the active map without changing it (used to capture before an await). */
35
- export function captureMap(): Map<object, unknown> | null {
36
- return _g[MAP_KEY];
37
- }
38
-
39
- const UNSUSPEND_KEY = "__hadarsUnsuspend";
40
-
41
- /**
42
- * Capture the current __hadarsUnsuspend slot alongside captureMap() before
43
- * an async boundary. Because useServerData reads this global, concurrent
44
- * renders would corrupt each other's cache if it weren't restored after every
45
- * await continuation — exactly like the context map itself.
46
- */
47
- export function captureUnsuspend(): unknown {
48
- return _g[UNSUSPEND_KEY];
49
- }
50
-
51
- /** Restore a previously captured __hadarsUnsuspend slot after an await. */
52
- export function restoreUnsuspend(u: unknown): void {
53
- _g[UNSUSPEND_KEY] = u;
54
- }
55
-
56
- /** Read the current value for a context within the active render. */
57
- export function getContextValue<T>(context: object): T {
58
- const map: Map<object, unknown> | null = _g[MAP_KEY];
59
- if (map && map.has(context)) return map.get(context) as T;
60
- const c = context as any;
61
- return ("_defaultValue" in c ? c._defaultValue : c._currentValue) as T;
62
- }
63
-
64
- /**
65
- * Push a new Provider value into the active map.
66
- * Returns the previous value so the caller can restore it on exit.
67
- */
68
- export function pushContextValue(context: object, value: unknown): unknown {
69
- let map: Map<object, unknown> | null = _g[MAP_KEY];
70
- // Lazily create the Map on the first Provider encountered — renders without
71
- // any Context.Provider never allocate a Map at all.
72
- if (map === null) {
73
- map = new Map();
74
- _g[MAP_KEY] = map;
75
- }
76
- const c = context as any;
77
- const prev = map.has(context)
78
- ? map.get(context)
79
- : ("_defaultValue" in c ? c._defaultValue : c._currentValue);
80
- map.set(context, value);
81
- return prev;
82
- }
83
-
84
- /** Restore a previously saved context value (called by Provider on exit). */
85
- export function popContextValue(context: object, prev: unknown): void {
86
- (_g[MAP_KEY] as Map<object, unknown> | null)?.set(context, prev);
87
- }
88
-
89
- // TreeContext matches React 19's representation exactly:
90
- // `id` is a packed bitfield with a leading sentinel `1` bit followed by tree
91
- // path slots. The most-recently-pushed slot occupies the HIGHEST non-sentinel
92
- // bits, matching React 19's Fizz `pushTreeContext` bit-packing order.
93
- // `overflow` accumulates segments that no longer fit in the 30-bit budget,
94
- // prepended newest-first (same as React 19).
95
- export interface TreeContext {
96
- id: number; // bitfield with sentinel; 1 = empty (just sentinel, no data)
97
- overflow: string; // base-32 partial path segments that overflowed
98
- }
99
-
100
- interface RenderState {
101
- currentTreeContext: TreeContext;
102
- localIdCounter: number;
103
- idPrefix: string;
104
- }
105
-
106
- const GLOBAL_KEY = "__slimReactRenderState";
107
- // React 19's initial context is { id: 1, overflow: "" } — sentinel bit only.
108
- const EMPTY: TreeContext = { id: 1, overflow: "" };
109
-
110
- /**
111
- * Module-level cache for the shared RenderState singleton.
112
- * Avoids a `globalThis[key]` property lookup on every push/pop/reset call
113
- * (which happens multiple times per component during rendering).
114
- * Both slim-react instances (direct import + SSR bundle copy) initialise the
115
- * same `globalThis[GLOBAL_KEY]` object and then each cache that same reference,
116
- * so correctness is preserved.
117
- */
118
- let _stateCache: RenderState | null = null;
119
-
120
- function s(): RenderState {
121
- if (_stateCache !== null) return _stateCache;
122
- if (!_g[GLOBAL_KEY]) {
123
- _g[GLOBAL_KEY] = { currentTreeContext: { ...EMPTY }, localIdCounter: 0, idPrefix: "" };
124
- }
125
- _stateCache = _g[GLOBAL_KEY] as RenderState;
126
- return _stateCache;
127
- }
128
-
129
- /**
130
- * Flat primitive stacks for pushTreeContext / popTreeContext.
131
- *
132
- * Instead of allocating a new `{ id, overflow }` object on every array child
133
- * (which is the hot path — called once per child in every array render), we
134
- * save the two scalar fields into parallel pre-allocated arrays and return a
135
- * numeric depth index. No heap objects are allocated in the push. Both arrays
136
- * grow lazily and are never shrunk, so after a few renders they stop growing.
137
- */
138
- const _treeIdStack: number[] = [];
139
- const _treeOvStack: string[] = [];
140
- let _treeDepth = 0;
141
-
142
- export function resetRenderState(idPrefix = "") {
143
- const st = s();
144
- // Mutate in place — avoids allocating a new TreeContext object each render.
145
- st.currentTreeContext.id = EMPTY.id;
146
- st.currentTreeContext.overflow = EMPTY.overflow;
147
- st.localIdCounter = 0;
148
- st.idPrefix = idPrefix;
149
- _treeDepth = 0;
150
- }
151
-
152
- export function setIdPrefix(prefix: string) {
153
- s().idPrefix = prefix;
154
- }
155
-
156
- /**
157
- * Push a new level onto the tree context — matches React 19's Fizz
158
- * `pushTreeContext` exactly:
159
- * - new slot occupies the HIGHER bit positions (above the old base data)
160
- * - on overflow, the LOWEST bits of the old data move to the overflow string
161
- * (rounded to a multiple of 5 so base-32 digits align on byte boundaries)
162
- */
163
- /**
164
- * Push a new tree-context level. Returns a numeric depth token (not an
165
- * object) so the caller can pop with zero heap allocation in the common case.
166
- */
167
- export function pushTreeContext(totalChildren: number, index: number): number {
168
- const st = s();
169
- const ctx = st.currentTreeContext;
170
- const depth = _treeDepth++;
171
-
172
- // Save current scalars into the flat stacks — no object allocation.
173
- _treeIdStack[depth] = ctx.id;
174
- _treeOvStack[depth] = ctx.overflow;
175
-
176
- const baseIdWithLeadingBit = ctx.id;
177
- const baseOverflow = ctx.overflow;
178
- const baseLength = 31 - Math.clz32(baseIdWithLeadingBit);
179
- let baseId = baseIdWithLeadingBit & ~(1 << baseLength);
180
-
181
- const slot = index + 1;
182
- const newBits = 32 - Math.clz32(totalChildren);
183
- const length = newBits + baseLength;
184
-
185
- // Mutate currentTreeContext in place — avoids allocating a new object.
186
- if (30 < length) {
187
- const overflowBits = baseLength - (baseLength % 5);
188
- const overflowStr = (baseId & ((1 << overflowBits) - 1)).toString(32);
189
- baseId >>= overflowBits;
190
- const newBaseLength = baseLength - overflowBits;
191
- ctx.id = (1 << (newBits + newBaseLength)) | (slot << newBaseLength) | baseId;
192
- ctx.overflow = overflowStr + baseOverflow;
193
- } else {
194
- ctx.id = (1 << length) | (slot << baseLength) | baseId;
195
- ctx.overflow = baseOverflow;
196
- }
197
- return depth;
198
- }
199
-
200
- export function popTreeContext(depth: number): void {
201
- const ctx = s().currentTreeContext;
202
- ctx.id = _treeIdStack[depth]!;
203
- ctx.overflow = _treeOvStack[depth]!;
204
- _treeDepth = depth;
205
- }
206
-
207
- export function pushComponentScope(): number {
208
- const st = s();
209
- const saved = st.localIdCounter;
210
- st.localIdCounter = 0;
211
- return saved;
212
- }
213
-
214
- export function popComponentScope(saved: number) {
215
- s().localIdCounter = saved;
216
- }
217
-
218
- /** True if the current component has called useId at least once. */
219
- export function componentCalledUseId(): boolean {
220
- return s().localIdCounter > 0;
221
- }
222
-
223
- export interface ContextSnapshot {
224
- tree: TreeContext;
225
- localId: number;
226
- treeDepth: number;
227
- /** Saved parent-context id values for the stack slots 0..treeDepth-1.
228
- * Required so that concurrent renders cannot corrupt popTreeContext calls
229
- * that run after an await continuation. */
230
- idStack: number[];
231
- ovStack: string[];
232
- }
233
-
234
- export function snapshotContext(): ContextSnapshot {
235
- const st = s();
236
- const ctx = st.currentTreeContext;
237
- const depth = _treeDepth;
238
- return {
239
- tree: { id: ctx.id, overflow: ctx.overflow },
240
- localId: st.localIdCounter,
241
- treeDepth: depth,
242
- // Snapshot the live stack so that popTreeContext reads correct saved values
243
- // even if another concurrent render's resetRenderState stomped the arrays.
244
- idStack: _treeIdStack.slice(0, depth),
245
- ovStack: _treeOvStack.slice(0, depth),
246
- };
247
- }
248
-
249
- export function restoreContext(snap: ContextSnapshot): void {
250
- const st = s();
251
- const ctx = st.currentTreeContext;
252
- ctx.id = snap.tree.id;
253
- ctx.overflow = snap.tree.overflow;
254
- st.localIdCounter = snap.localId;
255
- _treeDepth = snap.treeDepth;
256
- // Restore the stack so subsequent popTreeContext calls see the right values.
257
- for (let i = 0; i < snap.treeDepth; i++) {
258
- _treeIdStack[i] = snap.idStack[i]!;
259
- _treeOvStack[i] = snap.ovStack[i]!;
260
- }
261
- }
262
-
263
- /**
264
- * Produce the base-32 tree path string from the current context.
265
- * Strips the sentinel bit then concatenates stripped_id + overflow —
266
- * the same formula React 19 uses in both its Fizz SSR renderer and
267
- * the client-side `mountId`.
268
- */
269
- function getTreeId(): string {
270
- const { id, overflow } = s().currentTreeContext;
271
- if (id === 1) return overflow; // sentinel only → no local path segment
272
- const stripped = (id & ~(1 << (31 - Math.clz32(id)))).toString(32);
273
- return stripped + overflow;
274
- }
275
-
276
- /**
277
- * Generate a `useId`-compatible ID for the current call site.
278
- *
279
- * Format: `_R_<idPrefix><treeId>_` (React 19.2+)
280
- * with an optional `H<n>` suffix for the n-th useId call in the same
281
- * component (matching React 19's `localIdCounter` behaviour).
282
- *
283
- * React 19.2 uses `_R_<id>_` (underscore-delimited).
284
- * This matches React 19.2's output from both renderToString (Fizz) and
285
- * hydrateRoot, so SSR-generated IDs agree with client React during hydration.
286
- */
287
- export function makeId(): string {
288
- const st = s();
289
- const treeId = getTreeId();
290
- const n = st.localIdCounter++;
291
- let id = "_R_" + st.idPrefix + treeId;
292
- if (n > 0) id += "H" + n.toString(32);
293
- return id + "_";
294
- }
@@ -1,33 +0,0 @@
1
- // ---- Symbols ----
2
- // Use the same symbols as React so elements produced here are wire-compatible
3
- // with elements produced by the real React JSX runtime (e.g. when a library
4
- // uses React.createElement directly). This means the SSR bundle can be aliased
5
- // to slim-react without any element shape mismatch.
6
- //
7
- // React 19 introduced "react.transitional.element" as the canonical $$typeof for
8
- // elements created by createElement / the jsx-runtime. We keep the old
9
- // "react.element" as slim-react's own emission symbol (unchanged wire format for
10
- // SSR HTML — it makes no difference) and accept both in the renderer.
11
- export const SLIM_ELEMENT = Symbol.for("react.element");
12
- export const REACT19_ELEMENT = Symbol.for("react.transitional.element");
13
- export const FRAGMENT_TYPE = Symbol.for("react.fragment");
14
- export const SUSPENSE_TYPE = Symbol.for("react.suspense");
15
-
16
- // ---- Types ----
17
- export type ComponentFunction = (props: any) => SlimNode;
18
-
19
- export type SlimElement = {
20
- $$typeof: typeof SLIM_ELEMENT;
21
- type: string | ComponentFunction | symbol;
22
- props: Record<string, any>;
23
- key: string | number | null;
24
- };
25
-
26
- export type SlimNode =
27
- | SlimElement
28
- | string
29
- | number
30
- | boolean
31
- | null
32
- | undefined
33
- | SlimNode[];
@@ -1,113 +0,0 @@
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 & Record<string, unknown> = {
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
- };
83
-
84
- const actions: GatsbyActions = {
85
- createNode: (node) => store.createNode(node),
86
- deleteNode: ({ id }) => {
87
- // NodeStore doesn't implement delete — it's rarely needed on a first run.
88
- // Warn so plugin authors know the operation was ignored.
89
- console.warn(`[${pluginName}] deleteNode("${id}") called but is not supported by hadars — node will remain in the store.`);
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
- }
@@ -1,101 +0,0 @@
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
- let url: URL;
53
- try {
54
- url = new URL(req.url);
55
- } catch {
56
- return undefined;
57
- }
58
- if (url.pathname !== GRAPHQL_PATH) return undefined;
59
-
60
- // GET — serve GraphiQL IDE
61
- if (req.method === 'GET') {
62
- return new Response(GRAPHIQL_HTML, {
63
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
64
- });
65
- }
66
-
67
- // POST — execute GraphQL query
68
- if (req.method === 'POST') {
69
- let body: { query?: string; variables?: Record<string, unknown>; operationName?: string };
70
- try {
71
- body = await req.json();
72
- } catch {
73
- return new Response(JSON.stringify({ errors: [{ message: 'Invalid JSON body' }] }), {
74
- status: 400,
75
- headers: { 'Content-Type': 'application/json' },
76
- });
77
- }
78
-
79
- if (typeof body.query !== 'string' || !body.query.trim()) {
80
- return new Response(JSON.stringify({ errors: [{ message: 'Missing or invalid "query" field — must be a non-empty string' }] }), {
81
- status: 400,
82
- headers: { 'Content-Type': 'application/json' },
83
- });
84
- }
85
-
86
- try {
87
- const result = await executor(body.query, body.variables);
88
- return new Response(JSON.stringify(result), {
89
- headers: { 'Content-Type': 'application/json' },
90
- });
91
- } catch (err) {
92
- return new Response(JSON.stringify({ errors: [{ message: (err as Error).message }] }), {
93
- status: 500,
94
- headers: { 'Content-Type': 'application/json' },
95
- });
96
- }
97
- }
98
-
99
- return new Response('Method Not Allowed', { status: 405 });
100
- };
101
- }