hadars 0.2.0 → 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.
package/dist/cli.js CHANGED
@@ -5,9 +5,9 @@ import { spawn as spawn2 } from "node:child_process";
5
5
 
6
6
  // cli-lib.ts
7
7
  import { existsSync as existsSync3 } from "node:fs";
8
- import { mkdir, writeFile, unlink } from "node:fs/promises";
9
- import { resolve, join, dirname } from "node:path";
10
- import { fileURLToPath as fileURLToPath3 } from "node:url";
8
+ import { mkdir as mkdir2, writeFile as writeFile2, unlink, readFile as readFile2 } from "node:fs/promises";
9
+ import { resolve, join as join2, dirname as dirname2 } from "node:path";
10
+ import { fileURLToPath as fileURLToPath3, pathToFileURL as pathToFileURL2 } from "node:url";
11
11
 
12
12
  // src/utils/proxyHandler.tsx
13
13
  var cloneHeaders = (headers) => {
@@ -1174,29 +1174,33 @@ var getReactResponse = async (req, opts) => {
1174
1174
  head: { title: "Hadars App", meta: {}, link: {}, style: {}, script: {}, status: 200 }
1175
1175
  };
1176
1176
  let props = {
1177
- ...getInitProps ? await getInitProps(req) : {},
1177
+ ...getInitProps ? await getInitProps(req, opts.staticCtx) : {},
1178
1178
  location: req.location,
1179
1179
  context
1180
1180
  };
1181
1181
  const unsuspend = { cache: /* @__PURE__ */ new Map() };
1182
1182
  globalThis.__hadarsUnsuspend = unsuspend;
1183
+ globalThis.__hadarsContext = context;
1183
1184
  const element = createElement(App, props);
1184
1185
  try {
1185
1186
  await renderPreflight(element);
1186
1187
  } finally {
1187
1188
  globalThis.__hadarsUnsuspend = null;
1189
+ globalThis.__hadarsContext = null;
1188
1190
  }
1189
1191
  const status = context.head.status;
1190
1192
  const getAppBody = async () => {
1191
1193
  globalThis.__hadarsUnsuspend = unsuspend;
1194
+ globalThis.__hadarsContext = context;
1192
1195
  try {
1193
1196
  return await renderToString(element);
1194
1197
  } finally {
1195
1198
  globalThis.__hadarsUnsuspend = null;
1199
+ globalThis.__hadarsContext = null;
1196
1200
  }
1197
1201
  };
1198
1202
  const finalize = async () => {
1199
- const { context: _, ...restProps } = getFinalProps ? await getFinalProps(props) : props;
1203
+ const restProps = getFinalProps ? await getFinalProps(props) : props;
1200
1204
  const serverData = {};
1201
1205
  let hasServerData = false;
1202
1206
  for (const [key, entry] of unsuspend.cache) {
@@ -1792,6 +1796,11 @@ function buildSsrResponse(head, status, getAppBody, finalize, getPrecontentHtml)
1792
1796
  status
1793
1797
  });
1794
1798
  }
1799
+ async function buildSsrHtml(bodyHtml, clientProps, headHtml, getPrecontentHtml) {
1800
+ const [precontentHtml, postContent] = await Promise.resolve(getPrecontentHtml(headHtml));
1801
+ const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, "\\u003c");
1802
+ return precontentHtml + `<div id="app">${bodyHtml}</div><script id="hadars" type="application/json">${scriptContent}</script>` + postContent;
1803
+ }
1795
1804
  var makePrecontentHtmlGetter = (htmlFilePromise) => {
1796
1805
  let preHead = null;
1797
1806
  let postHead = null;
@@ -1887,6 +1896,357 @@ function createRenderCache(opts, handler) {
1887
1896
  };
1888
1897
  }
1889
1898
 
1899
+ // src/source/runner.ts
1900
+ import { EventEmitter as EventEmitter2 } from "node:events";
1901
+ import { createRequire } from "node:module";
1902
+
1903
+ // src/source/store.ts
1904
+ var NodeStore = class {
1905
+ byId = /* @__PURE__ */ new Map();
1906
+ byType = /* @__PURE__ */ new Map();
1907
+ createNode(node) {
1908
+ this.byId.set(node.id, node);
1909
+ const list = this.byType.get(node.internal.type) ?? [];
1910
+ const idx = list.findIndex((n) => n.id === node.id);
1911
+ if (idx >= 0) list[idx] = node;
1912
+ else list.push(node);
1913
+ this.byType.set(node.internal.type, list);
1914
+ }
1915
+ getNode(id) {
1916
+ return this.byId.get(id);
1917
+ }
1918
+ getNodes() {
1919
+ return Array.from(this.byId.values());
1920
+ }
1921
+ getNodesByType(type) {
1922
+ return this.byType.get(type) ?? [];
1923
+ }
1924
+ getTypes() {
1925
+ return Array.from(this.byType.keys());
1926
+ }
1927
+ };
1928
+
1929
+ // src/source/context.ts
1930
+ import { createHash } from "node:crypto";
1931
+ import { EventEmitter } from "node:events";
1932
+ function makeGatsbyContext(store, pluginName, pluginOptions = {}, emitter = new EventEmitter()) {
1933
+ const cacheMap = /* @__PURE__ */ new Map();
1934
+ const cache = {
1935
+ get: (key) => Promise.resolve(cacheMap.get(key)),
1936
+ set: (key, value) => {
1937
+ cacheMap.set(key, value);
1938
+ return Promise.resolve();
1939
+ }
1940
+ };
1941
+ const reporter = {
1942
+ info: (msg) => console.log(`[${pluginName}] ${msg}`),
1943
+ warn: (msg) => console.warn(`[${pluginName}] WARN: ${msg}`),
1944
+ error: (msg, err) => console.error(`[${pluginName}] ERROR: ${msg}`, err ?? ""),
1945
+ panic: (msg, err) => {
1946
+ console.error(`[${pluginName}] PANIC: ${msg}`, err ?? "");
1947
+ process.exit(1);
1948
+ },
1949
+ verbose: (msg) => {
1950
+ if (process.env.HADARS_VERBOSE) console.log(`[${pluginName}] ${msg}`);
1951
+ },
1952
+ activityTimer: (name) => ({
1953
+ start: () => {
1954
+ if (process.env.HADARS_VERBOSE) console.log(`[${pluginName}] \u25B6 ${name}`);
1955
+ },
1956
+ end: () => {
1957
+ if (process.env.HADARS_VERBOSE) console.log(`[${pluginName}] \u25A0 ${name}`);
1958
+ }
1959
+ }),
1960
+ // Gatsby 4+ reporter extras — used by some plugins but safe to no-op
1961
+ setErrorMap: () => {
1962
+ },
1963
+ log: (msg) => console.log(`[${pluginName}] ${msg}`),
1964
+ success: (msg) => console.log(`[${pluginName}] \u2713 ${msg}`)
1965
+ };
1966
+ const actions = {
1967
+ createNode: (node) => store.createNode(node),
1968
+ deleteNode: ({ id }) => {
1969
+ },
1970
+ touchNode: () => {
1971
+ }
1972
+ };
1973
+ return {
1974
+ actions,
1975
+ createNodeId: (input) => createHash("sha256").update(pluginName + input).digest("hex"),
1976
+ createContentDigest: (content) => {
1977
+ const str = typeof content === "string" ? content : JSON.stringify(content);
1978
+ return createHash("md5").update(str).digest("hex");
1979
+ },
1980
+ getNode: (id) => store.getNode(id),
1981
+ getNodes: () => store.getNodes(),
1982
+ getNodesByType: (t) => store.getNodesByType(t),
1983
+ cache,
1984
+ reporter,
1985
+ // Stubs for rarely-used Gatsby internals that plugins may destructure
1986
+ store: {},
1987
+ emitter,
1988
+ tracing: { startSpan: () => ({ finish: () => {
1989
+ } }) },
1990
+ schema: {},
1991
+ parentSpan: null
1992
+ };
1993
+ }
1994
+
1995
+ // src/source/runner.ts
1996
+ var SETTLE_IDLE_MS = 300;
1997
+ var SETTLE_TIMEOUT_MS = 1e4;
1998
+ function waitForSettle(getLastNodeTime) {
1999
+ return new Promise((resolve2) => {
2000
+ const deadline = Date.now() + SETTLE_TIMEOUT_MS;
2001
+ const check = () => {
2002
+ const idle = Date.now() - getLastNodeTime();
2003
+ if (idle >= SETTLE_IDLE_MS || Date.now() >= deadline) {
2004
+ resolve2();
2005
+ } else {
2006
+ setTimeout(check, 50);
2007
+ }
2008
+ };
2009
+ setTimeout(check, SETTLE_IDLE_MS);
2010
+ });
2011
+ }
2012
+ async function runSources(sources) {
2013
+ const store = new NodeStore();
2014
+ for (const entry of sources) {
2015
+ const { resolve: resolve2, options = {} } = entry;
2016
+ let mod;
2017
+ if (typeof resolve2 === "string") {
2018
+ const projectRequire = createRequire(process.cwd() + "/package.json");
2019
+ let pkgPath;
2020
+ try {
2021
+ pkgPath = projectRequire.resolve(`${resolve2}/gatsby-node`);
2022
+ } catch {
2023
+ pkgPath = projectRequire.resolve(resolve2);
2024
+ }
2025
+ mod = await import(pkgPath);
2026
+ } else {
2027
+ mod = resolve2;
2028
+ }
2029
+ if (typeof mod.sourceNodes !== "function") {
2030
+ const name = typeof resolve2 === "string" ? resolve2 : "(module)";
2031
+ console.warn(`[hadars] source plugin ${name} does not export sourceNodes \u2014 skipping`);
2032
+ continue;
2033
+ }
2034
+ const pluginName = typeof resolve2 === "string" ? resolve2 : "hadars-source";
2035
+ let lastNodeTime = Date.now();
2036
+ const trackingStore = new Proxy(store, {
2037
+ get(target, prop) {
2038
+ if (prop === "createNode") {
2039
+ return (node) => {
2040
+ lastNodeTime = Date.now();
2041
+ return target.createNode(node);
2042
+ };
2043
+ }
2044
+ return target[prop];
2045
+ }
2046
+ });
2047
+ const emitter = new EventEmitter2();
2048
+ const ctx = makeGatsbyContext(trackingStore, pluginName, options, emitter);
2049
+ try {
2050
+ await mod.sourceNodes(ctx, options);
2051
+ } catch (err) {
2052
+ throw new Error(
2053
+ `[hadars] source plugin "${pluginName}" threw during sourceNodes: ${err.message}`
2054
+ );
2055
+ }
2056
+ await waitForSettle(() => lastNodeTime);
2057
+ emitter.emit("BOOTSTRAP_FINISHED");
2058
+ await waitForSettle(() => lastNodeTime);
2059
+ }
2060
+ return store;
2061
+ }
2062
+
2063
+ // src/source/inference.ts
2064
+ function inferScalar(value) {
2065
+ if (typeof value === "boolean") return "Boolean";
2066
+ if (typeof value === "number") return Number.isInteger(value) ? "Int" : "Float";
2067
+ return "String";
2068
+ }
2069
+ function inferFieldShape(value, seenTypes) {
2070
+ if (value === null || value === void 0) {
2071
+ return { type: "String", nullable: true };
2072
+ }
2073
+ if (Array.isArray(value)) {
2074
+ const inner = value.length > 0 ? inferFieldShape(value[0], seenTypes) : { type: "String", nullable: true };
2075
+ return { type: `[${inner.type}]`, nullable: true };
2076
+ }
2077
+ if (typeof value === "object") {
2078
+ return { type: "String", nullable: true };
2079
+ }
2080
+ return { type: inferScalar(value), nullable: true };
2081
+ }
2082
+ var INTERNAL_FIELDS = /* @__PURE__ */ new Set(["id", "internal", "__typename", "parent", "children"]);
2083
+ var FILTERABLE_SCALARS = /* @__PURE__ */ new Set(["String", "Int", "Float", "Boolean", "ID"]);
2084
+ function buildTypeFields(nodes) {
2085
+ const fieldMap = /* @__PURE__ */ new Map();
2086
+ for (const node of nodes) {
2087
+ for (const [key, val] of Object.entries(node)) {
2088
+ if (INTERNAL_FIELDS.has(key)) continue;
2089
+ if (fieldMap.has(key)) continue;
2090
+ const { type } = inferFieldShape(val, /* @__PURE__ */ new Set());
2091
+ fieldMap.set(key, {
2092
+ name: key,
2093
+ type,
2094
+ filterable: FILTERABLE_SCALARS.has(type)
2095
+ });
2096
+ }
2097
+ }
2098
+ return Array.from(fieldMap.values());
2099
+ }
2100
+ function buildTypeSDL(typeName, fields) {
2101
+ const lines = [
2102
+ " id: ID!",
2103
+ ...fields.map((f) => ` ${f.name}: ${f.type}`)
2104
+ ];
2105
+ return `type ${typeName} {
2106
+ ${lines.join("\n")}
2107
+ }`;
2108
+ }
2109
+ function queryNames(typeName) {
2110
+ const lower = typeName.charAt(0).toLowerCase() + typeName.slice(1);
2111
+ return { single: lower, all: `all${typeName}` };
2112
+ }
2113
+ function buildSingleArgs(fields) {
2114
+ const args = [
2115
+ "id: ID",
2116
+ ...fields.filter((f) => f.filterable && f.name !== "id").map((f) => `${f.name}: ${f.type}`)
2117
+ ];
2118
+ return args.join(", ");
2119
+ }
2120
+ async function buildSchemaExecutor(store) {
2121
+ let gql;
2122
+ try {
2123
+ const { createRequire: createRequire2 } = await import("node:module");
2124
+ const projectRequire = createRequire2(process.cwd() + "/package.json");
2125
+ const graphqlPath = projectRequire.resolve("graphql");
2126
+ gql = await import(graphqlPath);
2127
+ } catch {
2128
+ return null;
2129
+ }
2130
+ const { buildSchema, graphql } = gql;
2131
+ const types = store.getTypes();
2132
+ if (types.length === 0) {
2133
+ const schema2 = buildSchema("type Query { _empty: String }");
2134
+ return (query, variables) => graphql({ schema: schema2, source: query, variableValues: variables });
2135
+ }
2136
+ const typeFields = new Map(
2137
+ types.map((typeName) => {
2138
+ const nodes = store.getNodesByType(typeName);
2139
+ return [typeName, buildTypeFields(nodes)];
2140
+ })
2141
+ );
2142
+ const typeSDLs = types.map(
2143
+ (typeName) => buildTypeSDL(typeName, typeFields.get(typeName))
2144
+ );
2145
+ const queryFields = types.map((typeName) => {
2146
+ const { single, all } = queryNames(typeName);
2147
+ const args = buildSingleArgs(typeFields.get(typeName));
2148
+ return [
2149
+ ` ${single}(${args}): ${typeName}`,
2150
+ ` ${all}: [${typeName}!]!`
2151
+ ].join("\n");
2152
+ });
2153
+ const sdl = [
2154
+ ...typeSDLs,
2155
+ `type Query {
2156
+ ${queryFields.join("\n")}
2157
+ }`
2158
+ ].join("\n\n");
2159
+ let schema;
2160
+ try {
2161
+ schema = buildSchema(sdl);
2162
+ } catch (err) {
2163
+ throw new Error(`[hadars] Failed to build GraphQL schema from node store: ${err.message}`);
2164
+ }
2165
+ const rootValue = {};
2166
+ for (const typeName of types) {
2167
+ const { single, all } = queryNames(typeName);
2168
+ rootValue[all] = () => store.getNodesByType(typeName);
2169
+ rootValue[single] = (args) => {
2170
+ const nodes = store.getNodesByType(typeName);
2171
+ return nodes.find(
2172
+ (node) => Object.entries(args).every(([k, v]) => v === void 0 || node[k] === v)
2173
+ ) ?? null;
2174
+ };
2175
+ }
2176
+ return (query, variables) => graphql({ schema, rootValue, source: query, variableValues: variables });
2177
+ }
2178
+
2179
+ // src/source/graphiql.ts
2180
+ var GRAPHQL_PATH = "/__hadars/graphql";
2181
+ var GRAPHIQL_HTML = `<!doctype html>
2182
+ <html lang="en">
2183
+ <head>
2184
+ <meta charset="utf-8">
2185
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2186
+ <title>GraphiQL \u2014 hadars</title>
2187
+ <style>
2188
+ * { box-sizing: border-box; margin: 0; padding: 0; }
2189
+ body { height: 100vh; overflow: hidden; }
2190
+ #graphiql { height: 100vh; }
2191
+ </style>
2192
+ <link rel="stylesheet" href="https://unpkg.com/graphiql@3/graphiql.min.css" />
2193
+ </head>
2194
+ <body>
2195
+ <div id="graphiql"></div>
2196
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
2197
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
2198
+ <script src="https://unpkg.com/graphiql@3/graphiql.min.js" crossorigin></script>
2199
+ <script>
2200
+ const root = ReactDOM.createRoot(document.getElementById('graphiql'));
2201
+ root.render(
2202
+ React.createElement(GraphiQL, {
2203
+ fetcher: GraphiQL.createFetcher({ url: '${GRAPHQL_PATH}' }),
2204
+ })
2205
+ );
2206
+ </script>
2207
+ </body>
2208
+ </html>`;
2209
+ function createGraphiqlHandler(executor) {
2210
+ return async (req) => {
2211
+ const url = new URL(req.url);
2212
+ if (url.pathname !== GRAPHQL_PATH) return void 0;
2213
+ if (req.method === "GET") {
2214
+ return new Response(GRAPHIQL_HTML, {
2215
+ headers: { "Content-Type": "text/html; charset=utf-8" }
2216
+ });
2217
+ }
2218
+ if (req.method === "POST") {
2219
+ let body;
2220
+ try {
2221
+ body = await req.json();
2222
+ } catch {
2223
+ return new Response(JSON.stringify({ errors: [{ message: "Invalid JSON body" }] }), {
2224
+ status: 400,
2225
+ headers: { "Content-Type": "application/json" }
2226
+ });
2227
+ }
2228
+ if (!body.query) {
2229
+ return new Response(JSON.stringify({ errors: [{ message: 'Missing "query" field' }] }), {
2230
+ status: 400,
2231
+ headers: { "Content-Type": "application/json" }
2232
+ });
2233
+ }
2234
+ try {
2235
+ const result = await executor(body.query, body.variables);
2236
+ return new Response(JSON.stringify(result), {
2237
+ headers: { "Content-Type": "application/json" }
2238
+ });
2239
+ } catch (err) {
2240
+ return new Response(JSON.stringify({ errors: [{ message: err.message }] }), {
2241
+ status: 500,
2242
+ headers: { "Content-Type": "application/json" }
2243
+ });
2244
+ }
2245
+ }
2246
+ return new Response("Method Not Allowed", { status: 405 });
2247
+ };
2248
+ }
2249
+
1890
2250
  // src/build.ts
1891
2251
  async function processHtmlTemplate(templatePath) {
1892
2252
  const html = await fs.readFile(templatePath, "utf-8");
@@ -2088,6 +2448,24 @@ var dev = async (options) => {
2088
2448
  const handleProxy = createProxyHandler(options);
2089
2449
  const handleWS = upgradeHandler(options);
2090
2450
  const handler = options.fetch;
2451
+ let handleGraphiql = null;
2452
+ let devStaticCtx;
2453
+ if (options.sources && options.sources.length > 0) {
2454
+ console.log(`[hadars] Running ${options.sources.length} source plugin(s)\u2026`);
2455
+ try {
2456
+ const store = await runSources(options.sources);
2457
+ const executor = await buildSchemaExecutor(store);
2458
+ if (executor) {
2459
+ devStaticCtx = { graphql: executor };
2460
+ handleGraphiql = createGraphiqlHandler(executor);
2461
+ console.log(`[hadars] GraphiQL available at http://localhost:${port}${GRAPHQL_PATH}`);
2462
+ } else {
2463
+ console.warn("[hadars] `graphql` package not found \u2014 GraphiQL disabled. Run: npm install graphql");
2464
+ }
2465
+ } catch (err) {
2466
+ console.error("[hadars] Source plugin error:", err);
2467
+ }
2468
+ }
2091
2469
  const entry = pathMod2.resolve(__dirname2, options.entry);
2092
2470
  const hmrPort = options.hmrPort ?? port + 1;
2093
2471
  const packageDir2 = pathMod2.dirname(fileURLToPath2(import.meta.url));
@@ -2260,6 +2638,10 @@ var dev = async (options) => {
2260
2638
  if (res) return res;
2261
2639
  }
2262
2640
  if (handleWS && handleWS(request, ctx)) return void 0;
2641
+ if (handleGraphiql) {
2642
+ const graphiqlRes = await handleGraphiql(req);
2643
+ if (graphiqlRes) return graphiqlRes;
2644
+ }
2263
2645
  const proxied = await handleProxy(request);
2264
2646
  if (proxied) return proxied;
2265
2647
  const url = new URL(request.url);
@@ -2283,7 +2665,8 @@ var dev = async (options) => {
2283
2665
  lang: "en",
2284
2666
  getInitProps,
2285
2667
  getFinalProps
2286
- }
2668
+ },
2669
+ staticCtx: devStaticCtx
2287
2670
  });
2288
2671
  if (request.headers.get("Accept") === "application/json") {
2289
2672
  const { clientProps } = await finalize();
@@ -2464,6 +2847,63 @@ var run = async (options) => {
2464
2847
  );
2465
2848
  };
2466
2849
 
2850
+ // src/static.ts
2851
+ import { cp, mkdir, writeFile } from "node:fs/promises";
2852
+ import { join, basename } from "node:path";
2853
+ async function renderStaticSite(opts) {
2854
+ const { ssrModule, htmlSource, staticSrc, paths, outputDir } = opts;
2855
+ const staticCtx = {
2856
+ graphql: opts.graphql ?? (() => Promise.reject(
2857
+ new Error("[hadars] No graphql executor configured. Add a `graphql` function to your hadars.config.")
2858
+ ))
2859
+ };
2860
+ const getPrecontentHtml = makePrecontentHtmlGetter(Promise.resolve(htmlSource));
2861
+ await mkdir(outputDir, { recursive: true });
2862
+ const rendered = [];
2863
+ const errors = [];
2864
+ for (const urlPath of paths) {
2865
+ try {
2866
+ const req = parseRequest(new Request("http://localhost" + urlPath));
2867
+ const { head, getAppBody, finalize } = await getReactResponse(req, {
2868
+ document: {
2869
+ body: ssrModule.default,
2870
+ getInitProps: ssrModule.getInitProps,
2871
+ getFinalProps: ssrModule.getFinalProps
2872
+ },
2873
+ staticCtx
2874
+ });
2875
+ const bodyHtml = await getAppBody();
2876
+ const { clientProps } = await finalize();
2877
+ const headHtml = buildHeadHtml(head);
2878
+ const staticClientProps = { ...clientProps, __hadarsStatic: true };
2879
+ const html = await buildSsrHtml(bodyHtml, staticClientProps, headHtml, getPrecontentHtml);
2880
+ const cleanPath = urlPath.replace(/\/$/, "");
2881
+ const pageDir = cleanPath === "" ? outputDir : join(outputDir, cleanPath);
2882
+ await mkdir(pageDir, { recursive: true });
2883
+ await writeFile(join(pageDir, "index.html"), html, "utf-8");
2884
+ const serverData = staticClientProps.__serverData ?? {};
2885
+ await writeFile(
2886
+ join(pageDir, "index.json"),
2887
+ JSON.stringify({ serverData }),
2888
+ "utf-8"
2889
+ );
2890
+ rendered.push(urlPath);
2891
+ } catch (err) {
2892
+ errors.push({
2893
+ path: urlPath,
2894
+ error: err instanceof Error ? err : new Error(String(err))
2895
+ });
2896
+ }
2897
+ }
2898
+ const staticDest = join(outputDir, "static");
2899
+ await mkdir(staticDest, { recursive: true });
2900
+ await cp(staticSrc, staticDest, {
2901
+ recursive: true,
2902
+ filter: (src) => basename(src) !== "out.html"
2903
+ });
2904
+ return { rendered, errors };
2905
+ }
2906
+
2467
2907
  // cli-lib.ts
2468
2908
  var SUPPORTED = ["hadars.config.js", "hadars.config.mjs", "hadars.config.cjs", "hadars.config.ts"];
2469
2909
  function findConfig(cwd) {
@@ -2497,6 +2937,130 @@ async function loadConfig(configPath) {
2497
2937
  const mod = await import(url);
2498
2938
  return mod && (mod.default ?? mod);
2499
2939
  }
2940
+ async function exportStatic(config, outputDir, cwd) {
2941
+ if (!config.paths) {
2942
+ console.error(
2943
+ "Error: `paths` is not defined in your hadars config.\nAdd a `paths` function that returns the list of URLs to pre-render:\n\n paths: () => ['/', '/about', '/contact']\n"
2944
+ );
2945
+ process.exit(1);
2946
+ }
2947
+ console.log("Building hadars project...");
2948
+ await build({ ...config, mode: "production" });
2949
+ const ssrBundle = resolve(cwd, ".hadars", "index.ssr.js");
2950
+ const outHtml = resolve(cwd, ".hadars", "static", "out.html");
2951
+ const staticSrc = resolve(cwd, ".hadars", "static");
2952
+ if (!existsSync3(ssrBundle)) {
2953
+ console.error(`SSR bundle not found: ${ssrBundle}`);
2954
+ process.exit(1);
2955
+ }
2956
+ if (!existsSync3(outHtml)) {
2957
+ console.error(`HTML template not found: ${outHtml}`);
2958
+ process.exit(1);
2959
+ }
2960
+ const ssrModule = await import(pathToFileURL2(ssrBundle).href);
2961
+ const htmlSource = await readFile2(outHtml, "utf-8");
2962
+ const outDir = resolve(cwd, outputDir);
2963
+ let graphql = config.graphql;
2964
+ if (config.sources && config.sources.length > 0) {
2965
+ console.log(`Running ${config.sources.length} source plugin(s)...`);
2966
+ const store = await runSources(config.sources);
2967
+ if (!graphql) {
2968
+ const inferred = await buildSchemaExecutor(store);
2969
+ if (!inferred) {
2970
+ console.error(
2971
+ "Error: `graphql` package not found.\nSource plugins require graphql-js to be installed:\n\n npm install graphql\n"
2972
+ );
2973
+ process.exit(1);
2974
+ }
2975
+ graphql = inferred;
2976
+ console.log(`Schema inferred for types: ${store.getTypes().join(", ") || "(none)"}`);
2977
+ }
2978
+ }
2979
+ const staticCtx = {
2980
+ graphql: graphql ?? (() => Promise.reject(
2981
+ new Error("[hadars] No graphql executor configured. Add a `graphql` function to your hadars.config.")
2982
+ ))
2983
+ };
2984
+ const paths = await config.paths(staticCtx);
2985
+ console.log(`Pre-rendering ${paths.length} page(s)...`);
2986
+ const { rendered, errors } = await renderStaticSite({
2987
+ ssrModule,
2988
+ htmlSource,
2989
+ staticSrc,
2990
+ paths,
2991
+ outputDir: outDir,
2992
+ graphql
2993
+ });
2994
+ for (const p of rendered) console.log(` [200] ${p}`);
2995
+ for (const { path: path2, error } of errors) console.error(` [ERR] ${path2}: ${error.message}`);
2996
+ console.log(`
2997
+ Exported to ${outputDir}/`);
2998
+ if (errors.length > 0) console.log(` ${errors.length} page(s) failed`);
2999
+ console.log(`
3000
+ Serve locally:`);
3001
+ console.log(` npx serve ${outputDir}`);
3002
+ }
3003
+ async function bundleCloudflare(config, configPath, outputFile, cwd) {
3004
+ console.log("Building hadars project...");
3005
+ await build({ ...config, mode: "production" });
3006
+ const ssrBundle = resolve(cwd, ".hadars", "index.ssr.js");
3007
+ const outHtml = resolve(cwd, ".hadars", "static", "out.html");
3008
+ if (!existsSync3(ssrBundle)) {
3009
+ console.error(`SSR bundle not found: ${ssrBundle}`);
3010
+ process.exit(1);
3011
+ }
3012
+ if (!existsSync3(outHtml)) {
3013
+ console.error(`HTML template not found: ${outHtml}`);
3014
+ process.exit(1);
3015
+ }
3016
+ const cloudflareModule = resolve(dirname2(fileURLToPath3(import.meta.url)), "cloudflare.js");
3017
+ const shimPath = join2(cwd, `.hadars-cloudflare-shim-${Date.now()}.ts`);
3018
+ const shim = [
3019
+ `import * as ssrModule from ${JSON.stringify(ssrBundle)};`,
3020
+ `import outHtml from ${JSON.stringify(outHtml)};`,
3021
+ `import { createCloudflareHandler } from ${JSON.stringify(cloudflareModule)};`,
3022
+ `import config from ${JSON.stringify(configPath)};`,
3023
+ `export default createCloudflareHandler(config as any, { ssrModule: ssrModule as any, outHtml });`
3024
+ ].join("\n") + "\n";
3025
+ await writeFile2(shimPath, shim, "utf-8");
3026
+ try {
3027
+ const { build: esbuild } = await import("esbuild");
3028
+ console.log(`Bundling Cloudflare Worker \u2192 ${outputFile}`);
3029
+ await esbuild({
3030
+ entryPoints: [shimPath],
3031
+ bundle: true,
3032
+ // 'browser' avoids Node.js built-in shims; CF Workers uses Web APIs.
3033
+ // If you use node:* APIs in your app code, add nodejs_compat to wrangler.toml.
3034
+ platform: "browser",
3035
+ format: "esm",
3036
+ target: ["es2022"],
3037
+ outfile: outputFile,
3038
+ sourcemap: false,
3039
+ loader: { ".html": "text", ".tsx": "tsx", ".ts": "ts" },
3040
+ // @rspack/* is build-time only — never imported at Worker runtime.
3041
+ external: ["@rspack/*"],
3042
+ // Cloudflare Workers supports the Web Crypto API natively; suppress
3043
+ // esbuild's attempt to polyfill node:crypto.
3044
+ define: { "global": "globalThis" }
3045
+ });
3046
+ console.log(`Cloudflare Worker bundle written to ${outputFile}`);
3047
+ console.log(`
3048
+ Deploy instructions:`);
3049
+ console.log(` 1. Ensure wrangler.toml points to the output file:`);
3050
+ console.log(` name = "my-app"`);
3051
+ console.log(` main = "${outputFile}"`);
3052
+ console.log(` compatibility_date = "2024-09-23"`);
3053
+ console.log(` compatibility_flags = ["nodejs_compat"]`);
3054
+ console.log(` 2. Upload .hadars/static/ assets to R2 (or another CDN):`);
3055
+ console.log(` wrangler r2 object put my-bucket/assets/ --file .hadars/static/ --recursive`);
3056
+ console.log(` 3. Add a route rule in wrangler.toml to send *.js / *.css to R2`);
3057
+ console.log(` and all other requests to the Worker.`);
3058
+ console.log(` 4. Deploy: wrangler deploy`);
3059
+ } finally {
3060
+ await unlink(shimPath).catch(() => {
3061
+ });
3062
+ }
3063
+ }
2500
3064
  async function bundleLambda(config, configPath, outputFile, cwd) {
2501
3065
  console.log("Building hadars project...");
2502
3066
  await build({ ...config, mode: "production" });
@@ -2510,8 +3074,8 @@ async function bundleLambda(config, configPath, outputFile, cwd) {
2510
3074
  console.error(`HTML template not found: ${outHtml}`);
2511
3075
  process.exit(1);
2512
3076
  }
2513
- const lambdaModule = resolve(dirname(fileURLToPath3(import.meta.url)), "lambda.js");
2514
- const shimPath = join(cwd, `.hadars-lambda-shim-${Date.now()}.ts`);
3077
+ const lambdaModule = resolve(dirname2(fileURLToPath3(import.meta.url)), "lambda.js");
3078
+ const shimPath = join2(cwd, `.hadars-lambda-shim-${Date.now()}.ts`);
2515
3079
  const shim = [
2516
3080
  `import * as ssrModule from ${JSON.stringify(ssrBundle)};`,
2517
3081
  `import outHtml from ${JSON.stringify(outHtml)};`,
@@ -2519,7 +3083,7 @@ async function bundleLambda(config, configPath, outputFile, cwd) {
2519
3083
  `import config from ${JSON.stringify(configPath)};`,
2520
3084
  `export const handler = createLambdaHandler(config as any, { ssrModule: ssrModule as any, outHtml });`
2521
3085
  ].join("\n") + "\n";
2522
- await writeFile(shimPath, shim, "utf-8");
3086
+ await writeFile2(shimPath, shim, "utf-8");
2523
3087
  try {
2524
3088
  const { build: esbuild } = await import("esbuild");
2525
3089
  console.log(`Bundling Lambda handler \u2192 ${outputFile}`);
@@ -2598,7 +3162,7 @@ export default config;
2598
3162
  dist/
2599
3163
  `,
2600
3164
  "src/App.tsx": () => `import React from 'react';
2601
- import { HadarsContext, HadarsHead, type HadarsApp } from 'hadars';
3165
+ import { HadarsHead, type HadarsApp } from 'hadars';
2602
3166
 
2603
3167
  const css = \`
2604
3168
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -2710,15 +3274,15 @@ const css = \`
2710
3274
 
2711
3275
  \`;
2712
3276
 
2713
- const App: HadarsApp<{}> = ({ context }) => {
3277
+ const App: HadarsApp<{}> = () => {
2714
3278
  const [count, setCount] = React.useState(0);
2715
3279
 
2716
3280
  return (
2717
- <HadarsContext context={context}>
3281
+ <>
2718
3282
  <HadarsHead status={200}>
2719
3283
  <title>My App</title>
2720
- <meta id="viewport" name="viewport" content="width=device-width, initial-scale=1" />
2721
- <style id="app-styles" dangerouslySetInnerHTML={{ __html: css }} />
3284
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
3285
+ <style data-id="app-styles" dangerouslySetInnerHTML={{ __html: css }} />
2722
3286
  </HadarsHead>
2723
3287
 
2724
3288
  <nav className="nav">
@@ -2776,7 +3340,7 @@ const App: HadarsApp<{}> = ({ context }) => {
2776
3340
  </div>
2777
3341
  </div>
2778
3342
 
2779
- </HadarsContext>
3343
+ </>
2780
3344
  );
2781
3345
  };
2782
3346
 
@@ -2790,10 +3354,10 @@ async function createProject(name, cwd) {
2790
3354
  process.exit(1);
2791
3355
  }
2792
3356
  console.log(`Creating hadars project in ${dir}`);
2793
- await mkdir(join(dir, "src"), { recursive: true });
3357
+ await mkdir2(join2(dir, "src"), { recursive: true });
2794
3358
  for (const [file, template] of Object.entries(TEMPLATES)) {
2795
3359
  const content = template(name);
2796
- await writeFile(join(dir, file), content, "utf-8");
3360
+ await writeFile2(join2(dir, file), content, "utf-8");
2797
3361
  console.log(` created ${file}`);
2798
3362
  }
2799
3363
  console.log(`
@@ -2805,7 +3369,7 @@ Done! Next steps:
2805
3369
  `);
2806
3370
  }
2807
3371
  function usage() {
2808
- console.log("Usage: hadars <new <name> | dev | build | run | export lambda [output.mjs]>");
3372
+ console.log("Usage: hadars <new <name> | dev | build | run | export lambda [output.mjs] | export cloudflare [output.mjs] | export static [outDir]>");
2809
3373
  }
2810
3374
  async function runCli(argv, cwd = process.cwd()) {
2811
3375
  const cmd = argv[2];
@@ -2848,13 +3412,22 @@ async function runCli(argv, cwd = process.cwd()) {
2848
3412
  process.exit(0);
2849
3413
  case "export": {
2850
3414
  const subCmd = argv[3];
2851
- if (subCmd !== "lambda") {
2852
- console.error(`Unknown export target: ${subCmd ?? "(none)"}. Did you mean: hadars export lambda`);
3415
+ if (subCmd === "lambda") {
3416
+ const outputFile = resolve(cwd, argv[4] ?? "lambda.mjs");
3417
+ await bundleLambda(cfg, configPath, outputFile, cwd);
3418
+ process.exit(0);
3419
+ } else if (subCmd === "cloudflare") {
3420
+ const outputFile = resolve(cwd, argv[4] ?? "cloudflare.mjs");
3421
+ await bundleCloudflare(cfg, configPath, outputFile, cwd);
3422
+ process.exit(0);
3423
+ } else if (subCmd === "static") {
3424
+ const outDirArg = argv[4] ?? "out";
3425
+ await exportStatic(cfg, outDirArg, cwd);
3426
+ process.exit(0);
3427
+ } else {
3428
+ console.error(`Unknown export target: ${subCmd ?? "(none)"}. Supported: lambda, cloudflare, static`);
2853
3429
  process.exit(1);
2854
3430
  }
2855
- const outputFile = resolve(cwd, argv[4] ?? "lambda.mjs");
2856
- await bundleLambda(cfg, configPath, outputFile, cwd);
2857
- process.exit(0);
2858
3431
  }
2859
3432
  case "run":
2860
3433
  console.log("Running project...");