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/README.md CHANGED
@@ -19,27 +19,30 @@ Bring your own router (or none), keep your components as plain React, and get SS
19
19
  ## Benchmarks
20
20
 
21
21
  <!-- BENCHMARK_START -->
22
- > Last run: 2026-03-18 · 60s · 200 connections · Bun runtime
23
- > hadars is **7.8x faster** in requests/sec
22
+ > Last run: 2026-03-21 · 60s · 100 connections · Bun runtime
23
+ > hadars is **8.7x faster** in requests/sec
24
24
 
25
25
  **Throughput** (autocannon, 60s)
26
26
 
27
27
  | Metric | hadars | Next.js |
28
28
  |---|---:|---:|
29
- | Requests/sec | **132** | 17 |
30
- | Latency median | **1490 ms** | 2757 ms |
31
- | Latency p99 | **2289 ms** | 6742 ms |
32
- | Throughput | **37.38** MB/s | 9.37 MB/s |
33
- | Build time | 0.7 s | 6.3 s |
29
+ | Requests/sec | **148** | 17 |
30
+ | Latency median | **647 ms** | 2712 ms |
31
+ | Latency p99 | **969 ms** | 6544 ms |
32
+ | Throughput | **42.23** MB/s | 9.29 MB/s |
33
+ | Peak RSS | 1062.8 MB | **472.3 MB** |
34
+ | Avg RSS | 799.7 MB | **404.7 MB** |
35
+ | Build time | 0.7 s | 6.0 s |
34
36
 
35
37
  **Page load** (Playwright · Chromium headless · median)
36
38
 
37
39
  | Metric | hadars | Next.js |
38
40
  |---|---:|---:|
39
- | TTFB | **22 ms** | 42 ms |
40
- | FCP | **124 ms** | 136 ms |
41
- | DOMContentLoaded | **88 ms** | 126 ms |
42
- | Load | **155 ms** | 173 ms |
41
+ | TTFB | **20 ms** | 43 ms |
42
+ | FCP | **100 ms** | 136 ms |
43
+ | DOMContentLoaded | **41 ms** | 127 ms |
44
+ | Load | **127 ms** | 174 ms |
45
+ | Peak RSS | 444.7 MB | **284.9 MB** |
43
46
  <!-- BENCHMARK_END -->
44
47
 
45
48
  ## Quick start
@@ -76,17 +79,17 @@ export default config;
76
79
  **src/App.tsx**
77
80
  ```tsx
78
81
  import React from 'react';
79
- import { HadarsContext, HadarsHead, type HadarsApp, type HadarsRequest } from 'hadars';
82
+ import { HadarsHead, type HadarsApp, type HadarsRequest } from 'hadars';
80
83
 
81
84
  interface Props { user: { name: string } }
82
85
 
83
- const App: HadarsApp<Props> = ({ user, context }) => (
84
- <HadarsContext context={context}>
86
+ const App: HadarsApp<Props> = ({ user }) => (
87
+ <>
85
88
  <HadarsHead status={200}>
86
89
  <title>Hello {user.name}</title>
87
90
  </HadarsHead>
88
91
  <h1>Hello, {user.name}!</h1>
89
- </HadarsContext>
92
+ </>
90
93
  );
91
94
 
92
95
  export const getInitProps = async (req: HadarsRequest): Promise<Props> => ({
@@ -111,8 +114,14 @@ hadars build
111
114
  # Serve the production build
112
115
  hadars run
113
116
 
117
+ # Pre-render every page to static HTML files (output goes to out/ by default)
118
+ hadars export static [outDir]
119
+
114
120
  # Bundle the app into a single self-contained Lambda .mjs file
115
121
  hadars export lambda [output.mjs]
122
+
123
+ # Bundle the app into a single self-contained Cloudflare Worker .mjs file
124
+ hadars export cloudflare [output.mjs]
116
125
  ```
117
126
 
118
127
  ## Features
@@ -175,6 +184,9 @@ const UserCard = ({ userId }: { userId: string }) => {
175
184
  | `htmlTemplate` | `string` | - | Path to a custom HTML template with `HADARS_HEAD` / `HADARS_BODY` markers |
176
185
  | `optimization` | `object` | - | Override rspack `optimization` for production client builds |
177
186
  | `cache` | `function` | - | SSR response cache for `run()` mode; return `{ key, ttl? }` to cache a request |
187
+ | `paths` | `function` | - | Returns URL list to pre-render with `hadars export static`; receives `HadarsStaticContext` |
188
+ | `sources` | `array` | - | Gatsby-compatible source plugins; hadars infers a GraphQL schema from their nodes |
189
+ | `graphql` | `function` | - | Custom GraphQL executor passed to `paths()` and `getInitProps()` as `ctx.graphql` |
178
190
 
179
191
  ### moduleRules example
180
192
 
@@ -210,6 +222,163 @@ const config: HadarsOptions = {
210
222
  export default config;
211
223
  ```
212
224
 
225
+ ## Static Export
226
+
227
+ > **Experimental.** Static export and Gatsby-compatible source plugins are new features. The API — including config shape, context object, and schema inference behaviour — may change in future releases without a major version bump.
228
+
229
+ Pre-render every page to a plain HTML file and deploy to any static host — no server required.
230
+
231
+ ```bash
232
+ # Output goes to out/ by default
233
+ hadars export static
234
+
235
+ # Custom output directory
236
+ hadars export static dist
237
+ ```
238
+
239
+ Add a `paths` function to `hadars.config.ts` that returns the list of URLs to pre-render:
240
+
241
+ ```ts
242
+ // hadars.config.ts
243
+ import type { HadarsOptions } from 'hadars';
244
+
245
+ export default {
246
+ entry: './src/App.tsx',
247
+ paths: () => ['/', '/about', '/contact'],
248
+ } satisfies HadarsOptions;
249
+ ```
250
+
251
+ Each URL is written as `<outDir>/<path>/index.html` plus an `index.json` sidecar so `useServerData` keeps working on client-side navigation without a live server. Static assets are copied from `.hadars/static/`.
252
+
253
+ ### Data in static pages
254
+
255
+ `getInitProps` receives a `HadarsStaticContext` as its second argument during static export. Use it to fetch data from a database, API, or GraphQL layer:
256
+
257
+ ```ts
258
+ import type { HadarsApp, HadarsRequest, HadarsStaticContext } from 'hadars';
259
+
260
+ export const getInitProps = async (
261
+ req: HadarsRequest,
262
+ ctx?: HadarsStaticContext,
263
+ ): Promise<Props> => {
264
+ if (!ctx) return { posts: [] };
265
+ const { data } = await ctx.graphql('{ allPost { id title } }');
266
+ return { posts: data?.allPost ?? [] };
267
+ };
268
+ ```
269
+
270
+ ## Source Plugins
271
+
272
+ hadars source plugins follow the same API as Gatsby's `sourceNodes` — so most existing Gatsby CMS source plugins work out of the box. Each plugin creates typed nodes in an in-memory store; hadars infers a GraphQL schema automatically and exposes it to `paths()` and `getInitProps()`.
273
+
274
+ During `hadars dev`, a GraphiQL IDE is available at `/__hadars/graphql` so you can explore the inferred schema while you build.
275
+
276
+ ### Install graphql
277
+
278
+ Schema inference requires `graphql` to be installed in your project:
279
+
280
+ ```bash
281
+ npm install graphql
282
+ ```
283
+
284
+ ### Config
285
+
286
+ ```ts
287
+ // hadars.config.ts
288
+ import type { HadarsOptions, HadarsStaticContext } from 'hadars';
289
+
290
+ export default {
291
+ entry: './src/App.tsx',
292
+
293
+ sources: [
294
+ {
295
+ resolve: 'gatsby-source-contentful',
296
+ options: {
297
+ spaceId: process.env.CONTENTFUL_SPACE_ID,
298
+ accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
299
+ },
300
+ },
301
+ ],
302
+
303
+ paths: async ({ graphql }: HadarsStaticContext) => {
304
+ const { data } = await graphql(`{ allContentfulBlogPost { slug } }`);
305
+ const slugs = data?.allContentfulBlogPost?.map((p: any) => p.slug) ?? [];
306
+ return ['/', ...slugs.map((s: string) => `/post/${s}`)];
307
+ },
308
+ } satisfies HadarsOptions;
309
+ ```
310
+
311
+ ### Local source plugin
312
+
313
+ Pass a pre-imported module instead of a package name to use a local plugin without publishing it to npm:
314
+
315
+ ```ts
316
+ // src/posts-source.ts
317
+ export async function sourceNodes(
318
+ { actions, createNodeId, createContentDigest }: any,
319
+ options: { dataDir: string } = {},
320
+ ) {
321
+ const { createNode } = actions;
322
+ const posts = await fetchPostsFromMyApi();
323
+ for (const post of posts) {
324
+ createNode({
325
+ ...post,
326
+ id: createNodeId(post.slug),
327
+ internal: { type: 'BlogPost', contentDigest: createContentDigest(post) },
328
+ });
329
+ }
330
+ }
331
+ ```
332
+
333
+ ```ts
334
+ // hadars.config.ts
335
+ import * as postsSource from './src/posts-source';
336
+
337
+ export default {
338
+ entry: './src/App.tsx',
339
+ sources: [{ resolve: postsSource }],
340
+ paths: async ({ graphql }) => {
341
+ const { data } = await graphql('{ allBlogPost { slug } }');
342
+ return ['/', ...(data?.allBlogPost ?? []).map((p: any) => `/post/${p.slug}`)];
343
+ },
344
+ } satisfies HadarsOptions;
345
+ ```
346
+
347
+ ### Inferred GraphQL schema
348
+
349
+ For each node type (e.g. `BlogPost`) you get two root queries:
350
+
351
+ | Query | Returns |
352
+ |---|---|
353
+ | `allBlogPost` | Every BlogPost node |
354
+ | `blogPost(id, slug, title, …)` | First node matching all supplied args |
355
+
356
+ All scalar fields are automatically added as optional lookup arguments, so you can do `blogPost(slug: "hello")` without knowing the hashed node id.
357
+
358
+ ### Custom GraphQL executor
359
+
360
+ Skip `sources` and provide a `graphql` executor directly for full control over resolvers:
361
+
362
+ ```ts
363
+ import { graphql, buildSchema } from 'graphql';
364
+ import type { HadarsOptions } from 'hadars';
365
+
366
+ const schema = buildSchema(`
367
+ type Post { id: ID! title: String slug: String }
368
+ type Query { allPost: [Post!]! }
369
+ `);
370
+
371
+ export default {
372
+ entry: './src/App.tsx',
373
+ graphql: (query, variables) =>
374
+ graphql({ schema, rootValue: { allPost: fetchPostsFromDb }, source: query, variableValues: variables }),
375
+ paths: async ({ graphql }) => {
376
+ const { data } = await graphql('{ allPost { slug } }');
377
+ return ['/', ...(data?.allPost ?? []).map((p: any) => `/post/${p.slug}`)];
378
+ },
379
+ } satisfies HadarsOptions;
380
+ ```
381
+
213
382
  ## AWS Lambda
214
383
 
215
384
  hadars apps can be deployed to AWS Lambda backed by API Gateway (HTTP API v2 or REST API v1).
@@ -282,6 +451,69 @@ Client-side code (anything that runs in the browser) is an exception: `process.e
282
451
 
283
452
  Client-side navigation sends a `GET <url>` request with `Accept: application/json` to refetch server data. The Lambda handler returns a JSON `{ serverData }` map for these requests — the same as the regular server does — so `useServerData` works identically in both deployment modes.
284
453
 
454
+ ## Cloudflare Workers
455
+
456
+ hadars apps can be deployed to Cloudflare Workers. The Worker handles SSR; static assets (JS, CSS, fonts) are served from R2 or another CDN.
457
+
458
+ ### Single-file bundle
459
+
460
+ `hadars export cloudflare` produces a self-contained `.mjs` Worker script. Unlike the Lambda adapter, no event format conversion is needed — Cloudflare Workers natively use the Web `Request`/`Response` API.
461
+
462
+ ```bash
463
+ # Outputs cloudflare.mjs in the current directory
464
+ hadars export cloudflare
465
+
466
+ # Custom output path
467
+ hadars export cloudflare dist/worker.mjs
468
+ ```
469
+
470
+ The command:
471
+ 1. Runs `hadars build`
472
+ 2. Generates an entry shim with static imports of the SSR module and `out.html`
473
+ 3. Bundles everything into a single ESM `.mjs` with esbuild (`platform: browser`, `target: es2022`)
474
+ 4. Prints wrangler deploy instructions
475
+
476
+ ### Deploy steps
477
+
478
+ 1. Add a `wrangler.toml` pointing at the output file:
479
+
480
+ ```toml
481
+ name = "my-app"
482
+ main = "cloudflare.mjs"
483
+ compatibility_date = "2024-09-23"
484
+ compatibility_flags = ["nodejs_compat"]
485
+ ```
486
+
487
+ 2. Upload `.hadars/static/` assets to R2 and configure routing rules so static file extensions (`*.js`, `*.css`, etc.) are served from R2 and all other requests go to the Worker.
488
+
489
+ 3. Deploy:
490
+
491
+ ```bash
492
+ wrangler deploy
493
+ ```
494
+
495
+ ### `createCloudflareHandler` API
496
+
497
+ ```ts
498
+ import { createCloudflareHandler, type CloudflareBundled } from 'hadars/cloudflare';
499
+ import * as ssrModule from './.hadars/index.ssr.js';
500
+ import outHtml from './.hadars/static/out.html';
501
+ import config from './hadars.config';
502
+
503
+ export default createCloudflareHandler(config, { ssrModule, outHtml });
504
+ ```
505
+
506
+ | Parameter | Type | Description |
507
+ |---|---|---|
508
+ | `options` | `HadarsOptions` | Same config object used for `dev`/`run` |
509
+ | `bundled` | `CloudflareBundled` | Pre-loaded SSR module + HTML template |
510
+
511
+ The returned object is a standard Cloudflare Workers export (`{ fetch(req, env, ctx) }`).
512
+
513
+ ### CPU time
514
+
515
+ hadars uses [slim-react](#slim-react) for SSR which is synchronous and typically renders a page in under 3 ms — well within Cloudflare's 10 ms free-plan CPU budget. Paid plans (Workers Paid) have no CPU time limit.
516
+
285
517
  ## slim-react
286
518
 
287
519
  hadars ships its own lightweight React-compatible SSR renderer called **slim-react** (`src/slim-react/`). It replaces `react-dom/server` on the server side entirely.
package/cli-lib.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import { existsSync } from 'node:fs'
2
- import { mkdir, writeFile, unlink } from 'node:fs/promises'
2
+ import { mkdir, writeFile, unlink, readFile } from 'node:fs/promises'
3
3
  import { resolve, join, dirname } from 'node:path'
4
- import { fileURLToPath } from 'node:url'
4
+ import { fileURLToPath, pathToFileURL } from 'node:url'
5
5
  import * as Hadars from './src/build'
6
- import type { HadarsOptions } from './src/types/hadars'
6
+ import type { HadarsOptions, HadarsEntryModule } from './src/types/hadars'
7
+ import { renderStaticSite } from './src/static'
8
+ import { runSources } from './src/source/runner'
9
+ import { buildSchemaExecutor } from './src/source/inference'
7
10
 
8
11
  const SUPPORTED = ['hadars.config.js', 'hadars.config.mjs', 'hadars.config.cjs', 'hadars.config.ts']
9
12
 
@@ -43,6 +46,163 @@ async function loadConfig(configPath: string): Promise<HadarsOptions> {
43
46
  return (mod && (mod.default ?? mod)) as HadarsOptions
44
47
  }
45
48
 
49
+ // ── hadars export static ─────────────────────────────────────────────────────
50
+
51
+ async function exportStatic(
52
+ config: HadarsOptions,
53
+ outputDir: string,
54
+ cwd: string,
55
+ ): Promise<void> {
56
+ if (!config.paths) {
57
+ console.error(
58
+ 'Error: `paths` is not defined in your hadars config.\n' +
59
+ 'Add a `paths` function that returns the list of URLs to pre-render:\n\n' +
60
+ ' paths: () => [\'/\', \'/about\', \'/contact\']\n',
61
+ )
62
+ process.exit(1)
63
+ }
64
+
65
+ console.log('Building hadars project...')
66
+ await Hadars.build({ ...config, mode: 'production' })
67
+
68
+ const ssrBundle = resolve(cwd, '.hadars', 'index.ssr.js')
69
+ const outHtml = resolve(cwd, '.hadars', 'static', 'out.html')
70
+ const staticSrc = resolve(cwd, '.hadars', 'static')
71
+
72
+ if (!existsSync(ssrBundle)) {
73
+ console.error(`SSR bundle not found: ${ssrBundle}`)
74
+ process.exit(1)
75
+ }
76
+ if (!existsSync(outHtml)) {
77
+ console.error(`HTML template not found: ${outHtml}`)
78
+ process.exit(1)
79
+ }
80
+
81
+ const ssrModule = await import(pathToFileURL(ssrBundle).href) as HadarsEntryModule<any>
82
+ const htmlSource = await readFile(outHtml, 'utf-8')
83
+ const outDir = resolve(cwd, outputDir)
84
+
85
+ // Run source plugins (if any) and build an auto-generated GraphQL executor.
86
+ // An explicit config.graphql always takes precedence over the inferred one.
87
+ let graphql = config.graphql
88
+ if (config.sources && config.sources.length > 0) {
89
+ console.log(`Running ${config.sources.length} source plugin(s)...`)
90
+ const store = await runSources(config.sources)
91
+ if (!graphql) {
92
+ const inferred = await buildSchemaExecutor(store)
93
+ if (!inferred) {
94
+ console.error(
95
+ 'Error: `graphql` package not found.\n' +
96
+ 'Source plugins require graphql-js to be installed:\n\n' +
97
+ ' npm install graphql\n',
98
+ )
99
+ process.exit(1)
100
+ }
101
+ graphql = inferred
102
+ console.log(`Schema inferred for types: ${store.getTypes().join(', ') || '(none)'}`)
103
+ }
104
+ }
105
+
106
+ const staticCtx = {
107
+ graphql: graphql ?? (() => Promise.reject(
108
+ new Error('[hadars] No graphql executor configured. Add a `graphql` function to your hadars.config.'),
109
+ )),
110
+ }
111
+
112
+ const paths = await config.paths(staticCtx)
113
+
114
+ console.log(`Pre-rendering ${paths.length} page(s)...`)
115
+
116
+ const { rendered, errors } = await renderStaticSite({
117
+ ssrModule,
118
+ htmlSource,
119
+ staticSrc,
120
+ paths,
121
+ outputDir: outDir,
122
+ graphql,
123
+ })
124
+
125
+ for (const p of rendered) console.log(` [200] ${p}`)
126
+ for (const { path, error } of errors) console.error(` [ERR] ${path}: ${error.message}`)
127
+
128
+ console.log(`\nExported to ${outputDir}/`)
129
+ if (errors.length > 0) console.log(` ${errors.length} page(s) failed`)
130
+ console.log(`\nServe locally:`)
131
+ console.log(` npx serve ${outputDir}`)
132
+ }
133
+
134
+ // ── hadars export cloudflare ─────────────────────────────────────────────────
135
+
136
+ async function bundleCloudflare(
137
+ config: HadarsOptions,
138
+ configPath: string,
139
+ outputFile: string,
140
+ cwd: string,
141
+ ): Promise<void> {
142
+ console.log('Building hadars project...')
143
+ await Hadars.build({ ...config, mode: 'production' })
144
+
145
+ const ssrBundle = resolve(cwd, '.hadars', 'index.ssr.js')
146
+ const outHtml = resolve(cwd, '.hadars', 'static', 'out.html')
147
+
148
+ if (!existsSync(ssrBundle)) {
149
+ console.error(`SSR bundle not found: ${ssrBundle}`)
150
+ process.exit(1)
151
+ }
152
+ if (!existsSync(outHtml)) {
153
+ console.error(`HTML template not found: ${outHtml}`)
154
+ process.exit(1)
155
+ }
156
+
157
+ // Resolve cloudflare.js from the dist/ directory (sibling of cli.js).
158
+ const cloudflareModule = resolve(dirname(fileURLToPath(import.meta.url)), 'cloudflare.js')
159
+ const shimPath = join(cwd, `.hadars-cloudflare-shim-${Date.now()}.ts`)
160
+ const shim = [
161
+ `import * as ssrModule from ${JSON.stringify(ssrBundle)};`,
162
+ `import outHtml from ${JSON.stringify(outHtml)};`,
163
+ `import { createCloudflareHandler } from ${JSON.stringify(cloudflareModule)};`,
164
+ `import config from ${JSON.stringify(configPath)};`,
165
+ `export default createCloudflareHandler(config as any, { ssrModule: ssrModule as any, outHtml });`,
166
+ ].join('\n') + '\n'
167
+ await writeFile(shimPath, shim, 'utf-8')
168
+
169
+ try {
170
+ const { build: esbuild } = await import('esbuild')
171
+ console.log(`Bundling Cloudflare Worker → ${outputFile}`)
172
+ await esbuild({
173
+ entryPoints: [shimPath],
174
+ bundle: true,
175
+ // 'browser' avoids Node.js built-in shims; CF Workers uses Web APIs.
176
+ // If you use node:* APIs in your app code, add nodejs_compat to wrangler.toml.
177
+ platform: 'browser',
178
+ format: 'esm',
179
+ target: ['es2022'],
180
+ outfile: outputFile,
181
+ sourcemap: false,
182
+ loader: { '.html': 'text', '.tsx': 'tsx', '.ts': 'ts' },
183
+ // @rspack/* is build-time only — never imported at Worker runtime.
184
+ external: ['@rspack/*'],
185
+ // Cloudflare Workers supports the Web Crypto API natively; suppress
186
+ // esbuild's attempt to polyfill node:crypto.
187
+ define: { 'global': 'globalThis' },
188
+ })
189
+ console.log(`Cloudflare Worker bundle written to ${outputFile}`)
190
+ console.log(`\nDeploy instructions:`)
191
+ console.log(` 1. Ensure wrangler.toml points to the output file:`)
192
+ console.log(` name = "my-app"`)
193
+ console.log(` main = "${outputFile}"`)
194
+ console.log(` compatibility_date = "2024-09-23"`)
195
+ console.log(` compatibility_flags = ["nodejs_compat"]`)
196
+ console.log(` 2. Upload .hadars/static/ assets to R2 (or another CDN):`)
197
+ console.log(` wrangler r2 object put my-bucket/assets/ --file .hadars/static/ --recursive`)
198
+ console.log(` 3. Add a route rule in wrangler.toml to send *.js / *.css to R2`)
199
+ console.log(` and all other requests to the Worker.`)
200
+ console.log(` 4. Deploy: wrangler deploy`)
201
+ } finally {
202
+ await unlink(shimPath).catch(() => {})
203
+ }
204
+ }
205
+
46
206
  // ── hadars export lambda ────────────────────────────────────────────────────
47
207
 
48
208
  async function bundleLambda(
@@ -172,7 +332,7 @@ dist/
172
332
 
173
333
  'src/App.tsx': () =>
174
334
  `import React from 'react';
175
- import { HadarsContext, HadarsHead, type HadarsApp } from 'hadars';
335
+ import { HadarsHead, type HadarsApp } from 'hadars';
176
336
 
177
337
  const css = \`
178
338
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -284,15 +444,15 @@ const css = \`
284
444
 
285
445
  \`;
286
446
 
287
- const App: HadarsApp<{}> = ({ context }) => {
447
+ const App: HadarsApp<{}> = () => {
288
448
  const [count, setCount] = React.useState(0);
289
449
 
290
450
  return (
291
- <HadarsContext context={context}>
451
+ <>
292
452
  <HadarsHead status={200}>
293
453
  <title>My App</title>
294
- <meta id="viewport" name="viewport" content="width=device-width, initial-scale=1" />
295
- <style id="app-styles" dangerouslySetInnerHTML={{ __html: css }} />
454
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
455
+ <style data-id="app-styles" dangerouslySetInnerHTML={{ __html: css }} />
296
456
  </HadarsHead>
297
457
 
298
458
  <nav className="nav">
@@ -350,7 +510,7 @@ const App: HadarsApp<{}> = ({ context }) => {
350
510
  </div>
351
511
  </div>
352
512
 
353
- </HadarsContext>
513
+ </>
354
514
  );
355
515
  };
356
516
 
@@ -388,7 +548,7 @@ Done! Next steps:
388
548
  // ── CLI entry ─────────────────────────────────────────────────────────────────
389
549
 
390
550
  function usage(): void {
391
- console.log('Usage: hadars <new <name> | dev | build | run | export lambda [output.mjs]>')
551
+ console.log('Usage: hadars <new <name> | dev | build | run | export lambda [output.mjs] | export cloudflare [output.mjs] | export static [outDir]>')
392
552
  }
393
553
 
394
554
  export async function runCli(argv: string[], cwd = process.cwd()): Promise<void> {
@@ -436,13 +596,22 @@ export async function runCli(argv: string[], cwd = process.cwd()): Promise<void>
436
596
  process.exit(0)
437
597
  case 'export': {
438
598
  const subCmd = argv[3]
439
- if (subCmd !== 'lambda') {
440
- console.error(`Unknown export target: ${subCmd ?? '(none)'}. Did you mean: hadars export lambda`)
599
+ if (subCmd === 'lambda') {
600
+ const outputFile = resolve(cwd, argv[4] ?? 'lambda.mjs')
601
+ await bundleLambda(cfg, configPath, outputFile, cwd)
602
+ process.exit(0)
603
+ } else if (subCmd === 'cloudflare') {
604
+ const outputFile = resolve(cwd, argv[4] ?? 'cloudflare.mjs')
605
+ await bundleCloudflare(cfg, configPath, outputFile, cwd)
606
+ process.exit(0)
607
+ } else if (subCmd === 'static') {
608
+ const outDirArg = argv[4] ?? 'out'
609
+ await exportStatic(cfg, outDirArg, cwd)
610
+ process.exit(0)
611
+ } else {
612
+ console.error(`Unknown export target: ${subCmd ?? '(none)'}. Supported: lambda, cloudflare, static`)
441
613
  process.exit(1)
442
614
  }
443
- const outputFile = resolve(cwd, argv[4] ?? 'lambda.mjs')
444
- await bundleLambda(cfg, configPath, outputFile, cwd)
445
- process.exit(0)
446
615
  }
447
616
  case 'run':
448
617
  console.log('Running project...')