hadars 0.1.35 → 0.1.37

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
@@ -88,6 +88,9 @@ hadars build
88
88
 
89
89
  # Serve the production build
90
90
  hadars run
91
+
92
+ # Bundle the app into a single self-contained Lambda .mjs file
93
+ hadars export lambda [output.mjs]
91
94
  ```
92
95
 
93
96
  ## Features
@@ -142,6 +145,8 @@ const UserCard = ({ userId }: { userId: string }) => {
142
145
  | `proxyCORS` | `boolean` | - | Inject CORS headers on proxied responses |
143
146
  | `define` | `Record` | - | Compile-time constants for rspack's DefinePlugin |
144
147
  | `swcPlugins` | `array` | - | Extra SWC plugins (e.g. Relay compiler) |
148
+
149
+ > **Environment variables in client bundles:** `process.env.*` references in client-side code are replaced at build time by rspack's DefinePlugin. They are **not** read at runtime in the browser. Use the `define` option to expose specific values, or keep env var access inside `getInitProps` / `fetch` (server-only code) so they are resolved at request time.
145
150
  | `moduleRules` | `array` | - | Extra rspack module rules appended to the built-in set (client + SSR) |
146
151
  | `fetch` | `function` | - | Custom fetch handler; return a `Response` to short-circuit SSR |
147
152
  | `websocket` | `object` | - | WebSocket handler (Bun only) |
@@ -184,6 +189,78 @@ const config: HadarsOptions = {
184
189
  export default config;
185
190
  ```
186
191
 
192
+ ## AWS Lambda
193
+
194
+ hadars apps can be deployed to AWS Lambda backed by API Gateway (HTTP API v2 or REST API v1).
195
+
196
+ ### File-based deployment
197
+
198
+ Run `hadars build`, then create a Lambda entry file that imports `createLambdaHandler` from `hadars/lambda`:
199
+
200
+ ```ts
201
+ // lambda-entry.ts
202
+ import { createLambdaHandler } from 'hadars/lambda';
203
+ import config from './hadars.config';
204
+
205
+ export const handler = createLambdaHandler(config);
206
+ ```
207
+
208
+ Deploy the entire project directory (including the `.hadars/` output folder) as your Lambda package. Static assets (JS, CSS, fonts) under `.hadars/static/` are served directly by the Lambda handler — for production, front the function with CloudFront and route static paths to an S3 origin instead.
209
+
210
+ ### Single-file bundle
211
+
212
+ `hadars export lambda` produces a completely self-contained `.mjs` file that requires no `.hadars/` directory on disk. The SSR module and HTML template are inlined at build time. Static assets must be served separately (S3 + CloudFront).
213
+
214
+ ```bash
215
+ # Outputs lambda.mjs in the current directory
216
+ hadars export lambda
217
+
218
+ # Custom output path
219
+ hadars export lambda dist/lambda.mjs
220
+ ```
221
+
222
+ The command:
223
+ 1. Runs `hadars build`
224
+ 2. Generates a temporary entry shim with static imports of the SSR module and `out.html`
225
+ 3. Bundles everything into a single ESM `.mjs` with esbuild (`.html` files loaded as text, Node built-ins kept external)
226
+ 4. Prints deploy instructions
227
+
228
+ **Deploy steps:**
229
+ 1. Upload the output `.mjs` as your Lambda function code
230
+ 2. Set the handler to `index.handler`
231
+ 3. Upload `.hadars/static/` assets to S3 and serve via CloudFront
232
+
233
+ ### `createLambdaHandler` API
234
+
235
+ ```ts
236
+ import { createLambdaHandler, type LambdaBundled } from 'hadars/lambda';
237
+
238
+ // File-based (reads .hadars/ at runtime)
239
+ export const handler = createLambdaHandler(config);
240
+
241
+ // Bundled (zero I/O — for use with `hadars export lambda` output)
242
+ import * as ssrModule from './.hadars/index.ssr.js';
243
+ import outHtml from './.hadars/static/out.html';
244
+ export const handler = createLambdaHandler(config, { ssrModule, outHtml });
245
+ ```
246
+
247
+ | Parameter | Type | Description |
248
+ |---|---|---|
249
+ | `options` | `HadarsOptions` | Same config object used for `dev`/`run` |
250
+ | `bundled` | `LambdaBundled` *(optional)* | Pre-loaded SSR module + HTML; eliminates all runtime file I/O |
251
+
252
+ The handler accepts both **API Gateway HTTP API (v2)** and **REST API (v1)** event formats. Binary responses (images, fonts, pre-compressed assets) are automatically base64-encoded.
253
+
254
+ ### Environment variables on Lambda
255
+
256
+ `process.env` is available in all server-side code (`getInitProps`, `fetch`, `cache`, etc.) and is resolved at runtime per invocation — Lambda injects env vars into the process before your handler runs.
257
+
258
+ Client-side code (anything that runs in the browser) is an exception: `process.env.*` references are substituted **at build time** by rspack. They will not reflect Lambda env vars set after the build. Expose runtime values to the client by returning them from `getInitProps` instead, or use the `define` option for values that are known at build time.
259
+
260
+ ### `useServerData` on Lambda
261
+
262
+ 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.
263
+
187
264
  ## slim-react
188
265
 
189
266
  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,6 +1,7 @@
1
1
  import { existsSync } from 'node:fs'
2
- import { mkdir, writeFile } from 'node:fs/promises'
3
- import { resolve, join } from 'node:path'
2
+ import { mkdir, writeFile, unlink } from 'node:fs/promises'
3
+ import { resolve, join, dirname } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
4
5
  import * as Hadars from './src/build'
5
6
  import type { HadarsOptions } from './src/types/hadars'
6
7
 
@@ -42,7 +43,81 @@ async function loadConfig(configPath: string): Promise<HadarsOptions> {
42
43
  return (mod && (mod.default ?? mod)) as HadarsOptions
43
44
  }
44
45
 
45
- // ── hadars new ────────────────────────────────────────────────────────────────
46
+ // ── hadars export lambda ────────────────────────────────────────────────────
47
+
48
+ async function bundleLambda(
49
+ config: HadarsOptions,
50
+ configPath: string,
51
+ outputFile: string,
52
+ cwd: string,
53
+ ): Promise<void> {
54
+ // 1. Ensure the hadars production build is up to date.
55
+ console.log('Building hadars project...')
56
+ await Hadars.build({ ...config, mode: 'production' })
57
+
58
+ // 2. Resolve paths.
59
+ const ssrBundle = resolve(cwd, '.hadars', 'index.ssr.js')
60
+ const outHtml = resolve(cwd, '.hadars', 'static', 'out.html')
61
+
62
+ if (!existsSync(ssrBundle)) {
63
+ console.error(`SSR bundle not found: ${ssrBundle}`)
64
+ process.exit(1)
65
+ }
66
+ if (!existsSync(outHtml)) {
67
+ console.error(`HTML template not found: ${outHtml}`)
68
+ process.exit(1)
69
+ }
70
+
71
+ // 3. Write a temporary entry shim that statically imports the SSR module
72
+ // and the HTML template so esbuild can inline both.
73
+ // Write the shim inside cwd so esbuild's module resolution finds local
74
+ // node_modules when walking up from the shim's directory.
75
+ // Use the absolute path to lambda.js (sibling of the CLI in dist/) so the
76
+ // shim doesn't depend on package name resolution at all.
77
+ const lambdaModule = resolve(dirname(fileURLToPath(import.meta.url)), 'lambda.js')
78
+ const shimPath = join(cwd, `.hadars-lambda-shim-${Date.now()}.ts`)
79
+ const shim = [
80
+ `import * as ssrModule from ${JSON.stringify(ssrBundle)};`,
81
+ `import outHtml from ${JSON.stringify(outHtml)};`,
82
+ `import { createLambdaHandler } from ${JSON.stringify(lambdaModule)};`,
83
+ `import config from ${JSON.stringify(configPath)};`,
84
+ `export const handler = createLambdaHandler(config as any, { ssrModule: ssrModule as any, outHtml });`,
85
+ ].join('\n') + '\n'
86
+ await writeFile(shimPath, shim, 'utf-8')
87
+
88
+ // 4. Bundle with esbuild.
89
+ try {
90
+ const { build: esbuild } = await import('esbuild')
91
+ console.log(`Bundling Lambda handler → ${outputFile}`)
92
+ await esbuild({
93
+ entryPoints: [shimPath],
94
+ bundle: true,
95
+ platform: 'node',
96
+ format: 'esm',
97
+ target: ['node20'],
98
+ outfile: outputFile,
99
+ sourcemap: false,
100
+ loader: { '.html': 'text', '.tsx': 'tsx', '.ts': 'ts' },
101
+ // @rspack/* contains native binaries and is build-time only —
102
+ // it is never imported at Lambda runtime, so mark it external.
103
+ // Everything else (React, hadars runtime, etc.) is bundled in to
104
+ // produce a truly self-contained single-file deployment.
105
+ external: ['@rspack/*'],
106
+ })
107
+ console.log(`Lambda bundle written to ${outputFile}`)
108
+ console.log(`\nDeploy instructions:`)
109
+ console.log(` 1. Create a staging directory with just this file:`)
110
+ console.log(` mkdir -p lambda-deploy && cp ${outputFile} lambda-deploy/lambda.mjs`)
111
+ console.log(` 2. Upload lambda-deploy/ as your Lambda function code`)
112
+ console.log(` 3. Set handler to: lambda.handler (runtime: Node.js 20.x)`)
113
+ console.log(` 4. Upload .hadars/static/ assets to S3 and serve via CloudFront`)
114
+ console.log(` (the Lambda handler does not serve static JS/CSS — route those to S3)`)
115
+ } finally {
116
+ await unlink(shimPath).catch(() => {})
117
+ }
118
+ }
119
+
120
+
46
121
 
47
122
  const TEMPLATES: Record<string, (name: string) => string> = {
48
123
  'package.json': (name: string) => JSON.stringify({
@@ -313,7 +388,7 @@ Done! Next steps:
313
388
  // ── CLI entry ─────────────────────────────────────────────────────────────────
314
389
 
315
390
  function usage(): void {
316
- console.log('Usage: hadars <new <name> | dev | build | run>')
391
+ console.log('Usage: hadars <new <name> | dev | build | run | export lambda [output.mjs]>')
317
392
  }
318
393
 
319
394
  export async function runCli(argv: string[], cwd = process.cwd()): Promise<void> {
@@ -334,7 +409,7 @@ export async function runCli(argv: string[], cwd = process.cwd()): Promise<void>
334
409
  return
335
410
  }
336
411
 
337
- if (!cmd || !['dev', 'build', 'run'].includes(cmd)) {
412
+ if (!cmd || !['dev', 'build', 'run', 'export'].includes(cmd)) {
338
413
  usage()
339
414
  process.exit(1)
340
415
  }
@@ -359,6 +434,16 @@ export async function runCli(argv: string[], cwd = process.cwd()): Promise<void>
359
434
  await build(cfg);
360
435
  console.log('Build complete')
361
436
  process.exit(0)
437
+ case 'export': {
438
+ const subCmd = argv[3]
439
+ if (subCmd !== 'lambda') {
440
+ console.error(`Unknown export target: ${subCmd ?? '(none)'}. Did you mean: hadars export lambda`)
441
+ process.exit(1)
442
+ }
443
+ const outputFile = resolve(cwd, argv[4] ?? 'lambda.mjs')
444
+ await bundleLambda(cfg, configPath, outputFile, cwd)
445
+ process.exit(0)
446
+ }
362
447
  case 'run':
363
448
  console.log('Running project...')
364
449
  await run(cfg);