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 +247 -15
- package/cli-lib.ts +184 -15
- package/dist/chunk-H72BZXOA.js +332 -0
- package/dist/cli.js +596 -23
- package/dist/cloudflare.cjs +1394 -0
- package/dist/cloudflare.d.cts +64 -0
- package/dist/cloudflare.d.ts +64 -0
- package/dist/cloudflare.js +68 -0
- package/dist/{hadars-Bh-V5YXg.d.cts → hadars-mKu5txjW.d.cts} +93 -37
- package/dist/{hadars-Bh-V5YXg.d.ts → hadars-mKu5txjW.d.ts} +93 -37
- package/dist/index.cjs +140 -164
- package/dist/index.d.cts +5 -11
- package/dist/index.d.ts +5 -11
- package/dist/index.js +140 -163
- package/dist/lambda.cjs +6 -2
- package/dist/lambda.d.cts +1 -2
- package/dist/lambda.d.ts +1 -2
- package/dist/lambda.js +10 -317
- package/dist/ssr-render-worker.js +3 -2
- package/dist/utils/Head.tsx +149 -195
- package/dist/utils/clientScript.tsx +9 -0
- package/index.ts +3 -0
- package/package.json +7 -2
- package/src/build.ts +29 -0
- package/src/cloudflare.ts +139 -0
- package/src/index.tsx +3 -3
- package/src/source/context.ts +113 -0
- package/src/source/graphiql.ts +96 -0
- package/src/source/inference.ts +188 -0
- package/src/source/runner.ts +114 -0
- package/src/source/store.ts +48 -0
- package/src/ssr-render-worker.ts +4 -1
- package/src/static.ts +106 -0
- package/src/types/hadars.ts +91 -2
- package/src/utils/Head.tsx +149 -195
- package/src/utils/clientScript.tsx +9 -0
- package/src/utils/response.tsx +10 -3
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-
|
|
23
|
-
> hadars is **
|
|
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 | **
|
|
30
|
-
| Latency median | **
|
|
31
|
-
| Latency p99 | **
|
|
32
|
-
| Throughput | **
|
|
33
|
-
|
|
|
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 | **
|
|
40
|
-
| FCP | **
|
|
41
|
-
| DOMContentLoaded | **
|
|
42
|
-
| Load | **
|
|
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 {
|
|
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
|
|
84
|
-
|
|
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
|
-
|
|
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 {
|
|
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<{}> = (
|
|
447
|
+
const App: HadarsApp<{}> = () => {
|
|
288
448
|
const [count, setCount] = React.useState(0);
|
|
289
449
|
|
|
290
450
|
return (
|
|
291
|
-
|
|
451
|
+
<>
|
|
292
452
|
<HadarsHead status={200}>
|
|
293
453
|
<title>My App</title>
|
|
294
|
-
<meta
|
|
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
|
-
|
|
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
|
|
440
|
-
|
|
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...')
|