hadars 0.4.1 → 0.4.2

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.
Files changed (58) hide show
  1. package/dist/{chunk-TV37IMRB.js → chunk-2TMQUXFL.js} +10 -10
  2. package/dist/{chunk-2J2L2H3H.js → chunk-NYLXE7T7.js} +6 -6
  3. package/dist/{chunk-OS3V4CPN.js → chunk-OZUZS2PD.js} +4 -4
  4. package/dist/cli.js +462 -496
  5. package/dist/cloudflare.cjs +11 -11
  6. package/dist/cloudflare.js +3 -3
  7. package/dist/index.d.cts +8 -4
  8. package/dist/index.d.ts +8 -4
  9. package/dist/lambda.cjs +11 -11
  10. package/dist/lambda.js +7 -7
  11. package/dist/loader.cjs +90 -54
  12. package/dist/slim-react/index.cjs +13 -13
  13. package/dist/slim-react/index.js +2 -2
  14. package/dist/slim-react/jsx-runtime.cjs +2 -4
  15. package/dist/slim-react/jsx-runtime.js +1 -1
  16. package/dist/ssr-render-worker.js +174 -161
  17. package/dist/ssr-watch.js +40 -74
  18. package/package.json +8 -10
  19. package/cli-lib.ts +0 -676
  20. package/cli.ts +0 -36
  21. package/index.ts +0 -17
  22. package/src/build.ts +0 -805
  23. package/src/cloudflare.ts +0 -140
  24. package/src/index.tsx +0 -41
  25. package/src/lambda.ts +0 -287
  26. package/src/slim-react/context.ts +0 -55
  27. package/src/slim-react/dispatcher.ts +0 -87
  28. package/src/slim-react/hooks.ts +0 -137
  29. package/src/slim-react/index.ts +0 -232
  30. package/src/slim-react/jsx-runtime.ts +0 -7
  31. package/src/slim-react/jsx.ts +0 -53
  32. package/src/slim-react/render.ts +0 -1101
  33. package/src/slim-react/renderContext.ts +0 -294
  34. package/src/slim-react/types.ts +0 -33
  35. package/src/source/context.ts +0 -113
  36. package/src/source/graphiql.ts +0 -101
  37. package/src/source/inference.ts +0 -260
  38. package/src/source/runner.ts +0 -138
  39. package/src/source/store.ts +0 -50
  40. package/src/ssr-render-worker.ts +0 -116
  41. package/src/ssr-watch.ts +0 -62
  42. package/src/static.ts +0 -109
  43. package/src/types/global.d.ts +0 -5
  44. package/src/types/hadars.ts +0 -350
  45. package/src/utils/Head.tsx +0 -462
  46. package/src/utils/clientScript.tsx +0 -71
  47. package/src/utils/cookies.ts +0 -16
  48. package/src/utils/loader.ts +0 -335
  49. package/src/utils/proxyHandler.tsx +0 -104
  50. package/src/utils/request.tsx +0 -9
  51. package/src/utils/response.tsx +0 -141
  52. package/src/utils/rspack.ts +0 -467
  53. package/src/utils/runtime.ts +0 -19
  54. package/src/utils/serve.ts +0 -155
  55. package/src/utils/ssrHandler.ts +0 -239
  56. package/src/utils/staticFile.ts +0 -43
  57. package/src/utils/template.html +0 -11
  58. package/src/utils/upgradeRequest.tsx +0 -19
package/cli-lib.ts DELETED
@@ -1,676 +0,0 @@
1
- import { existsSync } from 'node:fs'
2
- import { mkdir, writeFile, unlink, readFile } from 'node:fs/promises'
3
- import { resolve, join, dirname } from 'node:path'
4
- import { fileURLToPath, pathToFileURL } from 'node:url'
5
- import * as Hadars from './src/build'
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, buildSchemaSDL, introspectExecutorSDL } from './src/source/inference'
10
-
11
- const SUPPORTED = ['hadars.config.js', 'hadars.config.mjs', 'hadars.config.cjs', 'hadars.config.ts']
12
-
13
- function findConfig(cwd: string): string | null {
14
- for (const name of SUPPORTED) {
15
- const p = resolve(cwd, name)
16
- if (existsSync(p)) return p
17
- }
18
- return null
19
- }
20
-
21
- async function dev(config: HadarsOptions) {
22
- await Hadars.dev({
23
- ...config,
24
- baseURL: '',
25
- mode: 'development',
26
- });
27
- }
28
-
29
- async function build(config: HadarsOptions) {
30
- await Hadars.build({
31
- ...config,
32
- mode: 'production',
33
- });
34
- }
35
-
36
- async function run(config: HadarsOptions) {
37
- await Hadars.run({
38
- ...config,
39
- mode: 'production',
40
- });
41
- }
42
-
43
- async function loadConfig(configPath: string): Promise<HadarsOptions> {
44
- const url = `file://${configPath}`
45
- const mod = await import(url)
46
- return (mod && (mod.default ?? mod)) as HadarsOptions
47
- }
48
-
49
- // ── hadars export schema ─────────────────────────────────────────────────────
50
-
51
- async function exportSchema(
52
- config: HadarsOptions,
53
- outputFile: string,
54
- ): Promise<void> {
55
- let sdl: string | null = null
56
-
57
- if (config.sources && config.sources.length > 0) {
58
- console.log(`Running ${config.sources.length} source plugin(s)...`)
59
- const store = await runSources(config.sources)
60
- console.log(`Schema inferred for types: ${store.getTypes().join(', ') || '(none)'}`)
61
- sdl = await buildSchemaSDL(store)
62
- if (!sdl) {
63
- console.error(
64
- 'Error: `graphql` package not found.\n' +
65
- 'Source plugins require graphql-js to be installed:\n\n' +
66
- ' npm install graphql\n',
67
- )
68
- process.exit(1)
69
- }
70
- } else if (config.graphql) {
71
- console.log('Introspecting custom GraphQL executor...')
72
- sdl = await introspectExecutorSDL(config.graphql)
73
- if (!sdl) {
74
- console.error(
75
- 'Error: `graphql` package not found.\n' +
76
- 'Schema export requires graphql-js to be installed:\n\n' +
77
- ' npm install graphql\n',
78
- )
79
- process.exit(1)
80
- }
81
- } else {
82
- console.error(
83
- 'Error: no GraphQL source configured.\n' +
84
- 'Add `sources` or a `graphql` executor to your hadars.config.ts first.\n',
85
- )
86
- process.exit(1)
87
- }
88
-
89
- await writeFile(outputFile, sdl, 'utf-8')
90
- console.log(`Schema written to ${outputFile}`)
91
- console.log(`\nNext steps — generate TypeScript types with graphql-codegen:`)
92
- console.log(` npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations`)
93
- console.log(` npx graphql-codegen --schema ${outputFile} --documents "src/**/*.tsx" --out src/gql/`)
94
- }
95
-
96
- // ── hadars export static ─────────────────────────────────────────────────────
97
-
98
- async function exportStatic(
99
- config: HadarsOptions,
100
- outputDir: string,
101
- cwd: string,
102
- ): Promise<void> {
103
- if (!config.paths) {
104
- console.error(
105
- 'Error: `paths` is not defined in your hadars config.\n' +
106
- 'Add a `paths` function that returns the list of URLs to pre-render:\n\n' +
107
- ' paths: () => [\'/\', \'/about\', \'/contact\']\n',
108
- )
109
- process.exit(1)
110
- }
111
-
112
- console.log('Building hadars project...')
113
- await Hadars.build({ ...config, mode: 'production' })
114
-
115
- const ssrBundle = resolve(cwd, '.hadars', 'index.ssr.js')
116
- const outHtml = resolve(cwd, '.hadars', 'static', 'out.html')
117
- const staticSrc = resolve(cwd, '.hadars', 'static')
118
-
119
- if (!existsSync(ssrBundle)) {
120
- console.error(`SSR bundle not found: ${ssrBundle}`)
121
- process.exit(1)
122
- }
123
- if (!existsSync(outHtml)) {
124
- console.error(`HTML template not found: ${outHtml}`)
125
- process.exit(1)
126
- }
127
-
128
- const ssrModule = await import(pathToFileURL(ssrBundle).href) as HadarsEntryModule<any>
129
- const htmlSource = await readFile(outHtml, 'utf-8')
130
- const outDir = resolve(cwd, outputDir)
131
-
132
- // Run source plugins (if any) and build an auto-generated GraphQL executor.
133
- // An explicit config.graphql always takes precedence over the inferred one.
134
- let graphql = config.graphql
135
- if (config.sources && config.sources.length > 0) {
136
- console.log(`Running ${config.sources.length} source plugin(s)...`)
137
- const store = await runSources(config.sources)
138
- if (!graphql) {
139
- const inferred = await buildSchemaExecutor(store)
140
- if (!inferred) {
141
- console.error(
142
- 'Error: `graphql` package not found.\n' +
143
- 'Source plugins require graphql-js to be installed:\n\n' +
144
- ' npm install graphql\n',
145
- )
146
- process.exit(1)
147
- }
148
- graphql = inferred
149
- console.log(`Schema inferred for types: ${store.getTypes().join(', ') || '(none)'}`)
150
- }
151
- }
152
-
153
- const staticCtx = {
154
- graphql: graphql ?? (() => Promise.reject(
155
- new Error('[hadars] No graphql executor configured. Add a `graphql` function to your hadars.config.'),
156
- )),
157
- }
158
-
159
- const paths = await config.paths(staticCtx)
160
-
161
- console.log(`Pre-rendering ${paths.length} page(s)...`)
162
-
163
- const { rendered, errors } = await renderStaticSite({
164
- ssrModule,
165
- htmlSource,
166
- staticSrc,
167
- paths,
168
- outputDir: outDir,
169
- graphql,
170
- })
171
-
172
- for (const p of rendered) console.log(` [200] ${p}`)
173
- for (const { path, error } of errors) console.error(` [ERR] ${path}: ${error.message}`)
174
-
175
- console.log(`\nExported to ${outputDir}/`)
176
- if (errors.length > 0) console.log(` ${errors.length} page(s) failed`)
177
- console.log(`\nServe locally:`)
178
- console.log(` npx serve ${outputDir}`)
179
- }
180
-
181
- // ── hadars export cloudflare ─────────────────────────────────────────────────
182
-
183
- async function bundleCloudflare(
184
- config: HadarsOptions,
185
- configPath: string,
186
- outputFile: string,
187
- cwd: string,
188
- ): Promise<void> {
189
- console.log('Building hadars project...')
190
- await Hadars.build({ ...config, mode: 'production' })
191
-
192
- const ssrBundle = resolve(cwd, '.hadars', 'index.ssr.js')
193
- const outHtml = resolve(cwd, '.hadars', 'static', 'out.html')
194
-
195
- if (!existsSync(ssrBundle)) {
196
- console.error(`SSR bundle not found: ${ssrBundle}`)
197
- process.exit(1)
198
- }
199
- if (!existsSync(outHtml)) {
200
- console.error(`HTML template not found: ${outHtml}`)
201
- process.exit(1)
202
- }
203
-
204
- // Resolve cloudflare.js from the dist/ directory (sibling of cli.js).
205
- const cloudflareModule = resolve(dirname(fileURLToPath(import.meta.url)), 'cloudflare.js')
206
- const shimPath = join(cwd, `.hadars-cloudflare-shim-${Date.now()}.ts`)
207
- const shim = [
208
- `import * as ssrModule from ${JSON.stringify(ssrBundle)};`,
209
- `import outHtml from ${JSON.stringify(outHtml)};`,
210
- `import { createCloudflareHandler } from ${JSON.stringify(cloudflareModule)};`,
211
- `import config from ${JSON.stringify(configPath)};`,
212
- `export default createCloudflareHandler(config as any, { ssrModule: ssrModule as any, outHtml });`,
213
- ].join('\n') + '\n'
214
- await writeFile(shimPath, shim, 'utf-8')
215
-
216
- try {
217
- const { build: esbuild } = await import('esbuild')
218
- console.log(`Bundling Cloudflare Worker → ${outputFile}`)
219
- await esbuild({
220
- entryPoints: [shimPath],
221
- bundle: true,
222
- // 'browser' avoids Node.js built-in shims; CF Workers uses Web APIs.
223
- // If you use node:* APIs in your app code, add nodejs_compat to wrangler.toml.
224
- platform: 'browser',
225
- format: 'esm',
226
- target: ['es2022'],
227
- outfile: outputFile,
228
- sourcemap: false,
229
- loader: { '.html': 'text', '.tsx': 'tsx', '.ts': 'ts' },
230
- // @rspack/* is build-time only — never imported at Worker runtime.
231
- external: ['@rspack/*'],
232
- // Cloudflare Workers supports the Web Crypto API natively; suppress
233
- // esbuild's attempt to polyfill node:crypto.
234
- define: { 'global': 'globalThis' },
235
- })
236
- console.log(`Cloudflare Worker bundle written to ${outputFile}`)
237
- console.log(`\nDeploy instructions:`)
238
- console.log(` 1. Ensure wrangler.toml points to the output file:`)
239
- console.log(` name = "my-app"`)
240
- console.log(` main = "${outputFile}"`)
241
- console.log(` compatibility_date = "2024-09-23"`)
242
- console.log(` compatibility_flags = ["nodejs_compat"]`)
243
- console.log(` 2. Upload .hadars/static/ assets to R2 (or another CDN):`)
244
- console.log(` wrangler r2 object put my-bucket/assets/ --file .hadars/static/ --recursive`)
245
- console.log(` 3. Add a route rule in wrangler.toml to send *.js / *.css to R2`)
246
- console.log(` and all other requests to the Worker.`)
247
- console.log(` 4. Deploy: wrangler deploy`)
248
- } finally {
249
- await unlink(shimPath).catch(() => {})
250
- }
251
- }
252
-
253
- // ── hadars export lambda ────────────────────────────────────────────────────
254
-
255
- async function bundleLambda(
256
- config: HadarsOptions,
257
- configPath: string,
258
- outputFile: string,
259
- cwd: string,
260
- ): Promise<void> {
261
- // 1. Ensure the hadars production build is up to date.
262
- console.log('Building hadars project...')
263
- await Hadars.build({ ...config, mode: 'production' })
264
-
265
- // 2. Resolve paths.
266
- const ssrBundle = resolve(cwd, '.hadars', 'index.ssr.js')
267
- const outHtml = resolve(cwd, '.hadars', 'static', 'out.html')
268
-
269
- if (!existsSync(ssrBundle)) {
270
- console.error(`SSR bundle not found: ${ssrBundle}`)
271
- process.exit(1)
272
- }
273
- if (!existsSync(outHtml)) {
274
- console.error(`HTML template not found: ${outHtml}`)
275
- process.exit(1)
276
- }
277
-
278
- // 3. Write a temporary entry shim that statically imports the SSR module
279
- // and the HTML template so esbuild can inline both.
280
- // Write the shim inside cwd so esbuild's module resolution finds local
281
- // node_modules when walking up from the shim's directory.
282
- // Use the absolute path to lambda.js (sibling of the CLI in dist/) so the
283
- // shim doesn't depend on package name resolution at all.
284
- const lambdaModule = resolve(dirname(fileURLToPath(import.meta.url)), 'lambda.js')
285
- const shimPath = join(cwd, `.hadars-lambda-shim-${Date.now()}.ts`)
286
- const shim = [
287
- `import * as ssrModule from ${JSON.stringify(ssrBundle)};`,
288
- `import outHtml from ${JSON.stringify(outHtml)};`,
289
- `import { createLambdaHandler } from ${JSON.stringify(lambdaModule)};`,
290
- `import config from ${JSON.stringify(configPath)};`,
291
- `export const handler = createLambdaHandler(config as any, { ssrModule: ssrModule as any, outHtml });`,
292
- ].join('\n') + '\n'
293
- await writeFile(shimPath, shim, 'utf-8')
294
-
295
- // 4. Bundle with esbuild.
296
- try {
297
- const { build: esbuild } = await import('esbuild')
298
- console.log(`Bundling Lambda handler → ${outputFile}`)
299
- await esbuild({
300
- entryPoints: [shimPath],
301
- bundle: true,
302
- platform: 'node',
303
- format: 'esm',
304
- target: ['node20'],
305
- outfile: outputFile,
306
- sourcemap: false,
307
- loader: { '.html': 'text', '.tsx': 'tsx', '.ts': 'ts' },
308
- // @rspack/* contains native binaries and is build-time only —
309
- // it is never imported at Lambda runtime, so mark it external.
310
- // Everything else (React, hadars runtime, etc.) is bundled in to
311
- // produce a truly self-contained single-file deployment.
312
- external: ['@rspack/*'],
313
- })
314
- console.log(`Lambda bundle written to ${outputFile}`)
315
- console.log(`\nDeploy instructions:`)
316
- console.log(` 1. Create a staging directory with just this file:`)
317
- console.log(` mkdir -p lambda-deploy && cp ${outputFile} lambda-deploy/lambda.mjs`)
318
- console.log(` 2. Upload lambda-deploy/ as your Lambda function code`)
319
- console.log(` 3. Set handler to: lambda.handler (runtime: Node.js 20.x)`)
320
- console.log(` 4. Upload .hadars/static/ assets to S3 and serve via CloudFront`)
321
- console.log(` (the Lambda handler does not serve static JS/CSS — route those to S3)`)
322
- } finally {
323
- await unlink(shimPath).catch(() => {})
324
- }
325
- }
326
-
327
-
328
-
329
- const TEMPLATES: Record<string, (name: string) => string> = {
330
- 'package.json': (name: string) => JSON.stringify({
331
- name,
332
- version: '0.1.0',
333
- type: 'module',
334
- private: true,
335
- scripts: {
336
- dev: 'hadars dev',
337
- build: 'hadars build',
338
- start: 'hadars run',
339
- },
340
- dependencies: {
341
- 'hadars': 'latest',
342
- react: '^19.0.0',
343
- 'react-dom':'^19.0.0',
344
- },
345
- }, null, 2) + '\n',
346
-
347
- 'hadars.config.ts': () =>
348
- `import type { HadarsOptions } from 'hadars';
349
-
350
- const config: HadarsOptions = {
351
- entry: 'src/App.tsx',
352
- port: 3000,
353
- };
354
-
355
- export default config;
356
- `,
357
-
358
- 'tsconfig.json': () => JSON.stringify({
359
- compilerOptions: {
360
- lib: ['ESNext', 'DOM'],
361
- target: 'ESNext',
362
- module: 'Preserve',
363
- moduleDetection: 'force',
364
- jsx: 'react-jsx',
365
- moduleResolution: 'bundler',
366
- allowImportingTsExtensions: true,
367
- verbatimModuleSyntax: true,
368
- noEmit: true,
369
- strict: true,
370
- skipLibCheck: true,
371
- },
372
- }, null, 2) + '\n',
373
-
374
- '.gitignore': () =>
375
- `node_modules/
376
- .hadars/
377
- dist/
378
- `,
379
-
380
- 'src/App.tsx': () =>
381
- `import React from 'react';
382
- import { HadarsHead, type HadarsApp } from 'hadars';
383
-
384
- const css = \`
385
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
386
-
387
- body {
388
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
389
- background: #0f0f13;
390
- color: #e2e8f0;
391
- min-height: 100vh;
392
- }
393
-
394
- .nav {
395
- display: flex;
396
- align-items: center;
397
- justify-content: space-between;
398
- padding: 1rem 2rem;
399
- border-bottom: 1px solid #1e1e2e;
400
- }
401
- .nav-brand { font-weight: 700; font-size: 1.1rem; color: #a78bfa; letter-spacing: -0.02em; }
402
- .nav-links { display: flex; gap: 1.5rem; }
403
- .nav-links a { color: #94a3b8; text-decoration: none; font-size: 0.9rem; }
404
- .nav-links a:hover { color: #e2e8f0; }
405
-
406
- .hero {
407
- text-align: center;
408
- padding: 5rem 1rem 4rem;
409
- max-width: 680px;
410
- margin: 0 auto;
411
- }
412
- .hero-badge {
413
- display: inline-block;
414
- background: #1e1a2e;
415
- border: 1px solid #4c1d95;
416
- color: #a78bfa;
417
- font-size: 0.75rem;
418
- font-weight: 600;
419
- letter-spacing: 0.05em;
420
- text-transform: uppercase;
421
- padding: 0.3rem 0.8rem;
422
- border-radius: 999px;
423
- margin-bottom: 1.5rem;
424
- }
425
- .hero h1 {
426
- font-size: clamp(2rem, 5vw, 3.25rem);
427
- font-weight: 800;
428
- letter-spacing: -0.03em;
429
- line-height: 1.15;
430
- margin-bottom: 1rem;
431
- }
432
- .hero h1 span { color: #a78bfa; }
433
- .hero p {
434
- font-size: 1.1rem;
435
- color: #94a3b8;
436
- line-height: 1.7;
437
- margin-bottom: 2.5rem;
438
- }
439
- .hero-actions { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; }
440
- .btn {
441
- display: inline-flex;
442
- align-items: center;
443
- gap: 0.4rem;
444
- padding: 0.65rem 1.4rem;
445
- border-radius: 8px;
446
- font-size: 0.9rem;
447
- font-weight: 600;
448
- cursor: pointer;
449
- border: none;
450
- transition: opacity 0.15s, transform 0.1s;
451
- text-decoration: none;
452
- }
453
- .btn:hover { opacity: 0.85; transform: translateY(-1px); }
454
- .btn:active { transform: translateY(0); }
455
- .btn-primary { background: #7c3aed; color: #fff; }
456
- .btn-ghost { background: #1e1e2e; color: #e2e8f0; border: 1px solid #2d2d3e; }
457
-
458
- .features {
459
- display: grid;
460
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
461
- gap: 1rem;
462
- max-width: 900px;
463
- margin: 0 auto 4rem;
464
- padding: 0 1.5rem;
465
- }
466
- .card {
467
- background: #16161f;
468
- border: 1px solid #1e1e2e;
469
- border-radius: 12px;
470
- padding: 1.5rem;
471
- }
472
- .card-icon { font-size: 1.5rem; margin-bottom: 0.75rem; }
473
- .card h3 { font-size: 0.95rem; font-weight: 700; margin-bottom: 0.4rem; }
474
- .card p { font-size: 0.85rem; color: #64748b; line-height: 1.6; }
475
-
476
- .demo {
477
- max-width: 480px;
478
- margin: 0 auto 4rem;
479
- padding: 0 1.5rem;
480
- text-align: center;
481
- }
482
- .demo-box {
483
- background: #16161f;
484
- border: 1px solid #1e1e2e;
485
- border-radius: 12px;
486
- padding: 2rem;
487
- }
488
- .demo-box h2 { font-size: 0.8rem; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 1.25rem; }
489
- .counter { font-size: 3.5rem; font-weight: 800; color: #a78bfa; letter-spacing: -0.04em; margin-bottom: 1.25rem; }
490
- .demo-actions { display: flex; gap: 0.75rem; justify-content: center; }
491
-
492
- \`;
493
-
494
- const App: HadarsApp<{}> = () => {
495
- const [count, setCount] = React.useState(0);
496
-
497
- return (
498
- <>
499
- <HadarsHead status={200}>
500
- <title>My App</title>
501
- <meta name="viewport" content="width=device-width, initial-scale=1" />
502
- <style data-id="app-styles" dangerouslySetInnerHTML={{ __html: css }} />
503
- </HadarsHead>
504
-
505
- <nav className="nav">
506
- <span className="nav-brand">my-app</span>
507
- <div className="nav-links">
508
- <a href="https://github.com/dpostolachi/hadar" target="_blank" rel="noopener">github</a>
509
- </div>
510
- </nav>
511
-
512
- <section className="hero">
513
- <div className="hero-badge">built with hadars</div>
514
- <h1>Ship <span>React apps</span><br />at full speed</h1>
515
- <p>
516
- SSR out of the box, zero config, instant hot-reload.
517
- Edit <code>src/App.tsx</code> to get started.
518
- </p>
519
- <div className="hero-actions">
520
- <button className="btn btn-primary" onClick={() => setCount(c => c + 1)}>
521
- Try the counter ↓
522
- </button>
523
- </div>
524
- </section>
525
-
526
- <div className="features">
527
- <div className="card">
528
- <div className="card-icon">⚡</div>
529
- <h3>Server-side rendering</h3>
530
- <p>Pages render on the server and hydrate on the client — great for SEO and first paint.</p>
531
- </div>
532
- <div className="card">
533
- <div className="card-icon">🔥</div>
534
- <h3>Hot module reload</h3>
535
- <p>Changes in <code>src/App.tsx</code> reflect instantly in the browser during development.</p>
536
- </div>
537
- <div className="card">
538
- <div className="card-icon">📦</div>
539
- <h3>Zero config</h3>
540
- <p>One config file. Export a React component, run <code>hadars dev</code>, done.</p>
541
- </div>
542
- <div className="card">
543
- <div className="card-icon">🗄️</div>
544
- <h3>Server data hooks</h3>
545
- <p>Use <code>useServerData</code> to fetch data on the server without extra round-trips.</p>
546
- </div>
547
- </div>
548
-
549
- <div className="demo">
550
- <div className="demo-box">
551
- <h2>Client interactivity works</h2>
552
- <div className="counter">{count}</div>
553
- <div className="demo-actions">
554
- <button className="btn btn-ghost" onClick={() => setCount(c => c - 1)}>− dec</button>
555
- <button className="btn btn-primary" onClick={() => setCount(c => c + 1)}>+ inc</button>
556
- </div>
557
- </div>
558
- </div>
559
-
560
- </>
561
- );
562
- };
563
-
564
- export default App;
565
- `,
566
- }
567
-
568
- async function createProject(name: string, cwd: string): Promise<void> {
569
- const dir = resolve(cwd, name)
570
-
571
- if (existsSync(dir)) {
572
- console.error(`Directory already exists: ${dir}`)
573
- process.exit(1)
574
- }
575
-
576
- console.log(`Creating hadars project in ${dir}`)
577
-
578
- await mkdir(join(dir, 'src'), { recursive: true })
579
-
580
- for (const [file, template] of Object.entries(TEMPLATES)) {
581
- const content = template(name)
582
- await writeFile(join(dir, file), content, 'utf-8')
583
- console.log(` created ${file}`)
584
- }
585
-
586
- console.log(`
587
- Done! Next steps:
588
-
589
- cd ${name}
590
- npm install # or: bun install / pnpm install
591
- npm run dev # or: bun run dev
592
- `)
593
- }
594
-
595
- // ── CLI entry ─────────────────────────────────────────────────────────────────
596
-
597
- function usage(): void {
598
- console.log('Usage: hadars <new <name> | dev | build | run | export lambda [output.mjs] | export cloudflare [output.mjs] | export static [outDir] | export schema [schema.graphql]>')
599
- }
600
-
601
- export async function runCli(argv: string[], cwd = process.cwd()): Promise<void> {
602
- const cmd = argv[2]
603
-
604
- if (cmd === 'new') {
605
- const name = argv[3]
606
- if (!name) {
607
- console.error('Usage: hadars new <project-name>')
608
- process.exit(1)
609
- }
610
- try {
611
- await createProject(name, cwd)
612
- } catch (err: any) {
613
- console.error('Failed to create project:', err?.message ?? err)
614
- process.exit(2)
615
- }
616
- return
617
- }
618
-
619
- if (!cmd || !['dev', 'build', 'run', 'export'].includes(cmd)) {
620
- usage()
621
- process.exit(1)
622
- }
623
-
624
- const configPath = findConfig(cwd)
625
- if (!configPath) {
626
- console.log(`No hadars.config.* found in ${cwd}`)
627
- console.log('Proceeding with default behavior (no config)')
628
- return
629
- }
630
-
631
- try {
632
- const cfg = await loadConfig(configPath)
633
- console.log(`Loaded config from ${configPath}`)
634
- switch (cmd) {
635
- case 'dev':
636
- console.log('Starting development server...')
637
- await dev(cfg);
638
- break;
639
- case 'build':
640
- console.log('Building project...')
641
- await build(cfg);
642
- console.log('Build complete')
643
- process.exit(0)
644
- case 'export': {
645
- const subCmd = argv[3]
646
- if (subCmd === 'lambda') {
647
- const outputFile = resolve(cwd, argv[4] ?? 'lambda.mjs')
648
- await bundleLambda(cfg, configPath, outputFile, cwd)
649
- process.exit(0)
650
- } else if (subCmd === 'cloudflare') {
651
- const outputFile = resolve(cwd, argv[4] ?? 'cloudflare.mjs')
652
- await bundleCloudflare(cfg, configPath, outputFile, cwd)
653
- process.exit(0)
654
- } else if (subCmd === 'static') {
655
- const outDirArg = argv[4] ?? 'out'
656
- await exportStatic(cfg, outDirArg, cwd)
657
- process.exit(0)
658
- } else if (subCmd === 'schema') {
659
- const outputFile = resolve(cwd, argv[4] ?? 'schema.graphql')
660
- await exportSchema(cfg, outputFile)
661
- process.exit(0)
662
- } else {
663
- console.error(`Unknown export target: ${subCmd ?? '(none)'}. Supported: lambda, cloudflare, static, schema`)
664
- process.exit(1)
665
- }
666
- }
667
- case 'run':
668
- console.log('Running project...')
669
- await run(cfg);
670
- break;
671
- }
672
- } catch (err: any) {
673
- console.error('Failed to load config:', err?.message ?? err)
674
- process.exit(2)
675
- }
676
- }