hadars 0.2.1 → 0.2.2-rc.1
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 +177 -14
- package/cli-lib.ts +97 -5
- package/dist/{chunk-HWOLYLPF.js → chunk-H72BZXOA.js} +2 -2
- package/dist/cli.js +519 -16
- package/dist/cloudflare.cjs +2 -2
- package/dist/cloudflare.d.cts +1 -1
- package/dist/cloudflare.d.ts +1 -1
- package/dist/cloudflare.js +1 -1
- package/dist/{hadars-DEBSYAQl.d.cts → hadars-mKu5txjW.d.cts} +93 -2
- package/dist/{hadars-DEBSYAQl.d.ts → hadars-mKu5txjW.d.ts} +93 -2
- package/dist/index.cjs +11 -8
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +11 -8
- package/dist/lambda.cjs +2 -2
- package/dist/lambda.d.cts +1 -1
- package/dist/lambda.d.ts +1 -1
- package/dist/lambda.js +1 -1
- package/dist/utils/Head.tsx +17 -8
- package/dist/utils/clientScript.tsx +9 -0
- package/index.ts +3 -0
- package/package.json +1 -1
- package/src/build.ts +29 -0
- package/src/index.tsx +3 -0
- 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/static.ts +106 -0
- package/src/types/hadars.ts +91 -1
- package/src/utils/Head.tsx +17 -8
- package/src/utils/clientScript.tsx +9 -0
- package/src/utils/response.tsx +4 -3
package/README.md
CHANGED
|
@@ -19,30 +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 **8.
|
|
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
|
-
| Peak RSS |
|
|
34
|
-
| Avg RSS |
|
|
35
|
-
| Build time | 0.
|
|
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 |
|
|
36
36
|
|
|
37
37
|
**Page load** (Playwright · Chromium headless · median)
|
|
38
38
|
|
|
39
39
|
| Metric | hadars | Next.js |
|
|
40
40
|
|---|---:|---:|
|
|
41
|
-
| TTFB | **
|
|
42
|
-
| FCP | **
|
|
43
|
-
| DOMContentLoaded | **
|
|
44
|
-
| Load | **
|
|
45
|
-
| Peak RSS |
|
|
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** |
|
|
46
46
|
<!-- BENCHMARK_END -->
|
|
47
47
|
|
|
48
48
|
## Quick start
|
|
@@ -114,6 +114,9 @@ hadars build
|
|
|
114
114
|
# Serve the production build
|
|
115
115
|
hadars run
|
|
116
116
|
|
|
117
|
+
# Pre-render every page to static HTML files (output goes to out/ by default)
|
|
118
|
+
hadars export static [outDir]
|
|
119
|
+
|
|
117
120
|
# Bundle the app into a single self-contained Lambda .mjs file
|
|
118
121
|
hadars export lambda [output.mjs]
|
|
119
122
|
|
|
@@ -181,6 +184,9 @@ const UserCard = ({ userId }: { userId: string }) => {
|
|
|
181
184
|
| `htmlTemplate` | `string` | - | Path to a custom HTML template with `HADARS_HEAD` / `HADARS_BODY` markers |
|
|
182
185
|
| `optimization` | `object` | - | Override rspack `optimization` for production client builds |
|
|
183
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` |
|
|
184
190
|
|
|
185
191
|
### moduleRules example
|
|
186
192
|
|
|
@@ -216,6 +222,163 @@ const config: HadarsOptions = {
|
|
|
216
222
|
export default config;
|
|
217
223
|
```
|
|
218
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
|
+
|
|
219
382
|
## AWS Lambda
|
|
220
383
|
|
|
221
384
|
hadars apps can be deployed to AWS Lambda backed by API Gateway (HTTP API v2 or REST API v1).
|
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,91 @@ 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
|
+
|
|
46
134
|
// ── hadars export cloudflare ─────────────────────────────────────────────────
|
|
47
135
|
|
|
48
136
|
async function bundleCloudflare(
|
|
@@ -460,7 +548,7 @@ Done! Next steps:
|
|
|
460
548
|
// ── CLI entry ─────────────────────────────────────────────────────────────────
|
|
461
549
|
|
|
462
550
|
function usage(): void {
|
|
463
|
-
console.log('Usage: hadars <new <name> | dev | build | run | export lambda [output.mjs] | export cloudflare [output.mjs]>')
|
|
551
|
+
console.log('Usage: hadars <new <name> | dev | build | run | export lambda [output.mjs] | export cloudflare [output.mjs] | export static [outDir]>')
|
|
464
552
|
}
|
|
465
553
|
|
|
466
554
|
export async function runCli(argv: string[], cwd = process.cwd()): Promise<void> {
|
|
@@ -516,8 +604,12 @@ export async function runCli(argv: string[], cwd = process.cwd()): Promise<void>
|
|
|
516
604
|
const outputFile = resolve(cwd, argv[4] ?? 'cloudflare.mjs')
|
|
517
605
|
await bundleCloudflare(cfg, configPath, outputFile, cwd)
|
|
518
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)
|
|
519
611
|
} else {
|
|
520
|
-
console.error(`Unknown export target: ${subCmd ?? '(none)'}. Supported: lambda, cloudflare`)
|
|
612
|
+
console.error(`Unknown export target: ${subCmd ?? '(none)'}. Supported: lambda, cloudflare, static`)
|
|
521
613
|
process.exit(1)
|
|
522
614
|
}
|
|
523
615
|
}
|
|
@@ -171,7 +171,7 @@ var getReactResponse = async (req, opts) => {
|
|
|
171
171
|
head: { title: "Hadars App", meta: {}, link: {}, style: {}, script: {}, status: 200 }
|
|
172
172
|
};
|
|
173
173
|
let props = {
|
|
174
|
-
...getInitProps ? await getInitProps(req) : {},
|
|
174
|
+
...getInitProps ? await getInitProps(req, opts.staticCtx) : {},
|
|
175
175
|
location: req.location,
|
|
176
176
|
context
|
|
177
177
|
};
|
|
@@ -197,7 +197,7 @@ var getReactResponse = async (req, opts) => {
|
|
|
197
197
|
}
|
|
198
198
|
};
|
|
199
199
|
const finalize = async () => {
|
|
200
|
-
const
|
|
200
|
+
const restProps = getFinalProps ? await getFinalProps(props) : props;
|
|
201
201
|
const serverData = {};
|
|
202
202
|
let hasServerData = false;
|
|
203
203
|
for (const [key, entry] of unsuspend.cache) {
|