hadars 1.0.0 → 1.0.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.
package/dist/cli.js CHANGED
@@ -147,24 +147,6 @@ var SLIM_ELEMENT = Symbol.for("react.element");
147
147
  var REACT19_ELEMENT = Symbol.for("react.transitional.element");
148
148
  var FRAGMENT_TYPE = Symbol.for("react.fragment");
149
149
  var SUSPENSE_TYPE = Symbol.for("react.suspense");
150
- // src/slim-react/jsx.ts
151
- function createElement(type, props, ...children) {
152
- const normalizedProps = { ...props || {} };
153
- if (children.length === 1) {
154
- normalizedProps.children = children[0];
155
- } else if (children.length > 1) {
156
- normalizedProps.children = children;
157
- }
158
- const key = normalizedProps.key ?? null;
159
- delete normalizedProps.key;
160
- return {
161
- $$typeof: SLIM_ELEMENT,
162
- type,
163
- props: normalizedProps,
164
- key
165
- };
166
- }
167
-
168
150
  // src/slim-react/renderContext.ts
169
151
  var MAP_KEY = "__slimReactContextMap";
170
152
  var _g = globalThis;
@@ -189,8 +171,7 @@ function getContextValue(context) {
189
171
  const map = _g[MAP_KEY];
190
172
  if (map && map.has(context))
191
173
  return map.get(context);
192
- const c = context;
193
- return "_defaultValue" in c ? c._defaultValue : c._currentValue;
174
+ return context._currentValue;
194
175
  }
195
176
  function pushContextValue(context, value) {
196
177
  let map = _g[MAP_KEY];
@@ -198,8 +179,7 @@ function pushContextValue(context, value) {
198
179
  map = new Map;
199
180
  _g[MAP_KEY] = map;
200
181
  }
201
- const c = context;
202
- const prev = map.has(context) ? map.get(context) : ("_defaultValue" in c) ? c._defaultValue : c._currentValue;
182
+ const prev = map.has(context) ? map.get(context) : context._currentValue;
203
183
  map.set(context, value);
204
184
  return prev;
205
185
  }
@@ -304,14 +284,35 @@ function getTreeId() {
304
284
  const stripped = (id & ~(1 << 31 - Math.clz32(id))).toString(32);
305
285
  return stripped + overflow;
306
286
  }
287
+ var REACT_MAJOR = typeof __HADARS_REACT_MAJOR__ !== "undefined" ? parseInt(String(__HADARS_REACT_MAJOR__), 10) : 19;
288
+ var REACT_VERSION = REACT_MAJOR < 19 ? "18.3.1" : "19.1.1";
307
289
  function makeId() {
308
290
  const st = s();
309
291
  const treeId = getTreeId();
310
292
  const n = st.localIdCounter++;
311
- let id = "_R_" + st.idPrefix + treeId;
312
- if (n > 0)
313
- id += "H" + n.toString(32);
314
- return id + "_";
293
+ const suffix = n > 0 ? "H" + n.toString(32) : "";
294
+ if (REACT_MAJOR < 19) {
295
+ return ":" + st.idPrefix + "R" + treeId + suffix + ":";
296
+ }
297
+ return "_R_" + st.idPrefix + treeId + suffix + "_";
298
+ }
299
+
300
+ // src/slim-react/jsx.ts
301
+ function createElement(type, props, ...children) {
302
+ const normalizedProps = { ...props || {} };
303
+ if (children.length === 1) {
304
+ normalizedProps.children = children[0];
305
+ } else if (children.length > 1) {
306
+ normalizedProps.children = children;
307
+ }
308
+ const key = normalizedProps.key ?? null;
309
+ delete normalizedProps.key;
310
+ return {
311
+ $$typeof: SLIM_ELEMENT,
312
+ type,
313
+ props: normalizedProps,
314
+ key
315
+ };
315
316
  }
316
317
 
317
318
  // src/slim-react/hooks.ts
@@ -364,8 +365,24 @@ function use(usable) {
364
365
  }
365
366
  // src/slim-react/dispatcher.ts
366
367
  import * as ReactNS from "react";
367
- var _reactInternalsKey = "__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE";
368
- var _internals = ReactNS[_reactInternalsKey];
368
+ var _r19;
369
+ var _r18;
370
+ var _detected = false;
371
+ var _k19 = "__CLIENT_INTERNALS_DO_NOT_USE" + "_OR_WARN_USERS_THEY_CANNOT_UPGRADE";
372
+ var _k18 = "__SECRET_INTERNALS_DO_NOT_USE" + "_OR_YOU_WILL_BE_FIRED";
373
+ function _detect() {
374
+ if (_detected)
375
+ return;
376
+ _detected = true;
377
+ const r19 = ReactNS[_k19];
378
+ if (r19) {
379
+ _r19 = r19;
380
+ return;
381
+ }
382
+ const raw = ReactNS[_k18];
383
+ if (raw?.ReactCurrentDispatcher)
384
+ _r18 = raw;
385
+ }
369
386
  var slimDispatcher = {
370
387
  useId: makeId,
371
388
  readContext: (ctx) => getContextValue(ctx),
@@ -391,15 +408,25 @@ var slimDispatcher = {
391
408
  useHostTransitionStatus: () => false
392
409
  };
393
410
  function installDispatcher() {
394
- if (!_internals)
395
- return null;
396
- const prev = _internals.H;
397
- _internals.H = slimDispatcher;
398
- return prev;
411
+ _detect();
412
+ if (_r19) {
413
+ const prev = _r19.H;
414
+ _r19.H = slimDispatcher;
415
+ return prev;
416
+ }
417
+ if (_r18) {
418
+ const prev = _r18.ReactCurrentDispatcher.current;
419
+ _r18.ReactCurrentDispatcher.current = slimDispatcher;
420
+ return prev;
421
+ }
422
+ return null;
399
423
  }
400
424
  function restoreDispatcher(prev) {
401
- if (_internals)
402
- _internals.H = prev;
425
+ _detect();
426
+ if (_r19)
427
+ _r19.H = prev;
428
+ else if (_r18)
429
+ _r18.ReactCurrentDispatcher.current = prev;
403
430
  }
404
431
 
405
432
  // src/slim-react/render.ts
@@ -660,7 +687,7 @@ function writeAttributes(writer, props, isSvg, skip) {
660
687
  if (isSvg && key in SVG_ATTR_MAP) {
661
688
  attrName = SVG_ATTR_MAP[key];
662
689
  } else {
663
- attrName = key === "className" ? "class" : key === "htmlFor" ? "for" : key === "tabIndex" ? "tabindex" : key === "defaultValue" ? "value" : key === "defaultChecked" ? "checked" : key;
690
+ attrName = key === "className" ? "class" : key === "htmlFor" ? "for" : key === "tabIndex" ? "tabindex" : key === "defaultValue" ? "value" : key === "defaultChecked" ? "checked" : key === "contentEditable" && REACT_MAJOR < 19 ? "contenteditable" : key;
664
691
  }
665
692
  if (value === false || value == null) {
666
693
  if (value === false && (attrName.charCodeAt(0) === 97 && attrName.startsWith("aria-") || attrName.charCodeAt(0) === 100 && attrName.startsWith("data-"))) {
@@ -871,7 +898,8 @@ function renderComponent(type, props, writer, isSvg, _suspenseRetries = 0) {
871
898
  const LazyComp = resolved?.default ?? resolved;
872
899
  return renderComponent(LazyComp, props, writer, isSvg);
873
900
  }
874
- if (typeOf === REACT_CONSUMER) {
901
+ const isConsumer = typeOf === REACT_CONSUMER || typeOf === REACT_CONTEXT && "_context" in type && !("value" in props);
902
+ if (isConsumer) {
875
903
  const ctx2 = type._context;
876
904
  const value = ctx2 ? getContextValue(ctx2) : undefined;
877
905
  const result2 = typeof props.children === "function" ? props.children(value) : null;
@@ -2448,6 +2476,20 @@ var HadarsFolder = "./.hadars";
2448
2476
  var StaticPath = `${HadarsFolder}/static`;
2449
2477
  var HADARS_TMP_DIR = pathMod2.join(os.tmpdir(), "hadars");
2450
2478
  var ensureHadarsTmpDir = () => fs.mkdir(HADARS_TMP_DIR, { recursive: true });
2479
+ var readReactMajor = async () => {
2480
+ let dir = process.cwd();
2481
+ while (true) {
2482
+ try {
2483
+ const pkgPath = pathMod2.join(dir, "node_modules", "react", "package.json");
2484
+ const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
2485
+ return parseInt(pkg.version.split(".")[0], 10);
2486
+ } catch {}
2487
+ const parent = pathMod2.dirname(dir);
2488
+ if (parent === dir)
2489
+ return 19;
2490
+ dir = parent;
2491
+ }
2492
+ };
2451
2493
  var validateOptions = (options) => {
2452
2494
  if (!options.entry) {
2453
2495
  throw new Error("Entry file is required");
@@ -2567,6 +2609,8 @@ var dev = async (options) => {
2567
2609
  });
2568
2610
  const workerCmd = resolveWorkerCmd(packageDir2);
2569
2611
  console.log("Spawning SSR worker:", workerCmd.join(" "), "entry:", entry);
2612
+ const reactMajor = await readReactMajor();
2613
+ const ssrDefine = { __HADARS_REACT_MAJOR__: String(reactMajor), ...options.define };
2570
2614
  const child = spawn(workerCmd[0], [
2571
2615
  ...workerCmd.slice(1),
2572
2616
  `--entry=${entry}`,
@@ -2574,7 +2618,7 @@ var dev = async (options) => {
2574
2618
  `--outFile=${SSR_FILENAME}`,
2575
2619
  `--base=${baseURL}`,
2576
2620
  ...options.swcPlugins ? [`--swcPlugins=${JSON.stringify(options.swcPlugins)}`] : [],
2577
- ...options.define ? [`--define=${JSON.stringify(options.define)}`] : [],
2621
+ `--define=${JSON.stringify(ssrDefine)}`,
2578
2622
  ...options.moduleRules ? [`--moduleRules=${JSON.stringify(options.moduleRules, (_k, v) => v instanceof RegExp ? { __re: v.source, __flags: v.flags } : v)}`] : []
2579
2623
  ], { stdio: "pipe" });
2580
2624
  child.stdin?.end();
@@ -2748,6 +2792,7 @@ var build = async (options) => {
2748
2792
  const tmpFilePath = pathMod2.join(HADARS_TMP_DIR, `client-${Date.now()}.tsx`);
2749
2793
  await fs.writeFile(tmpFilePath, clientScript);
2750
2794
  const resolvedHtmlTemplate = options.htmlTemplate ? await processHtmlTemplate(pathMod2.resolve(__dirname3, options.htmlTemplate)) : undefined;
2795
+ const reactMajor = await readReactMajor();
2751
2796
  console.log("Building client and server bundles in parallel...");
2752
2797
  await Promise.all([
2753
2798
  compileEntry(tmpFilePath, {
@@ -2779,7 +2824,7 @@ var build = async (options) => {
2779
2824
  target: "node",
2780
2825
  mode: "production",
2781
2826
  swcPlugins: options.swcPlugins,
2782
- define: options.define,
2827
+ define: { __HADARS_REACT_MAJOR__: String(reactMajor), ...options.define },
2783
2828
  moduleRules: options.moduleRules,
2784
2829
  plugins: options.plugins,
2785
2830
  postcssPlugins: options.postcssPlugins
@@ -3184,8 +3229,166 @@ Deploy instructions:`);
3184
3229
  await unlink(shimPath).catch(() => {});
3185
3230
  }
3186
3231
  }
3187
- var TEMPLATES = {
3188
- "package.json": (name) => JSON.stringify({
3232
+ var _R = "\x1B[0m";
3233
+ var _B = "\x1B[1m";
3234
+ var _D = "\x1B[2m";
3235
+ var _C = "\x1B[36m";
3236
+ var _G = "\x1B[32m";
3237
+ var _UP_KEY = "\x1B[A";
3238
+ var _DOWN_KEY = "\x1B[B";
3239
+ var _HIDE = "\x1B[?25l";
3240
+ var _SHOW = "\x1B[?25h";
3241
+ var _cl = () => "\r\x1B[2K";
3242
+ var _up = (n) => n > 0 ? `\x1B[${n}A` : "";
3243
+ function _readKeys(handler) {
3244
+ return new Promise((resolve2) => {
3245
+ const { stdin } = process;
3246
+ const wasRaw = stdin.isTTY && stdin.isRaw;
3247
+ if (stdin.isTTY)
3248
+ stdin.setRawMode(true);
3249
+ stdin.resume();
3250
+ stdin.setEncoding("utf-8");
3251
+ const onData = (key) => {
3252
+ if (key === "\x03") {
3253
+ cleanup();
3254
+ process.stdout.write(_SHOW);
3255
+ process.exit(130);
3256
+ }
3257
+ if (handler(key))
3258
+ cleanup();
3259
+ };
3260
+ const cleanup = () => {
3261
+ stdin.removeListener("data", onData);
3262
+ if (stdin.isTTY && !wasRaw)
3263
+ stdin.setRawMode(false);
3264
+ stdin.pause();
3265
+ resolve2();
3266
+ };
3267
+ stdin.on("data", onData);
3268
+ });
3269
+ }
3270
+ async function promptRadio(question, options) {
3271
+ const out = process.stdout;
3272
+ let cursor = 0;
3273
+ const total = 1 + options.length;
3274
+ const render = (redraw) => {
3275
+ if (redraw)
3276
+ out.write(_up(total));
3277
+ out.write(`${_cl()} ${_B}${question}${_R}
3278
+ `);
3279
+ for (let i = 0;i < options.length; i++) {
3280
+ const arrow = i === cursor ? `${_C}❯${_R}` : " ";
3281
+ const text = i === cursor ? `${_B}${options[i]}${_R}` : `${_D}${options[i]}${_R}`;
3282
+ out.write(`${_cl()} ${arrow} ${text}
3283
+ `);
3284
+ }
3285
+ };
3286
+ out.write(_HIDE);
3287
+ render(false);
3288
+ await _readKeys((key) => {
3289
+ if (key === _UP_KEY && cursor > 0) {
3290
+ cursor--;
3291
+ render(true);
3292
+ } else if (key === _DOWN_KEY && cursor < options.length - 1) {
3293
+ cursor++;
3294
+ render(true);
3295
+ } else if (key === "\r") {
3296
+ return true;
3297
+ }
3298
+ return false;
3299
+ });
3300
+ out.write(_up(total));
3301
+ out.write(`${_cl()} ${question} ${_C}${_B}${options[cursor]}${_R}
3302
+ `);
3303
+ for (let i = 0;i < options.length; i++)
3304
+ out.write(`${_cl()}
3305
+ `);
3306
+ out.write(_up(options.length));
3307
+ out.write(_SHOW);
3308
+ return cursor;
3309
+ }
3310
+ async function promptMultiSelect(question, options) {
3311
+ const out = process.stdout;
3312
+ let cursor = 0;
3313
+ const selected = new Set;
3314
+ const total = 2 + options.length;
3315
+ const render = (redraw) => {
3316
+ if (redraw)
3317
+ out.write(_up(total));
3318
+ out.write(`${_cl()} ${_B}${question}${_R}
3319
+ `);
3320
+ out.write(`${_cl()} ${_D}↑↓ navigate · Space select · Enter confirm${_R}
3321
+ `);
3322
+ for (let i = 0;i < options.length; i++) {
3323
+ const arrow = i === cursor ? `${_C}❯${_R}` : " ";
3324
+ const box = selected.has(i) ? `${_G}●${_R}` : `${_D}○${_R}`;
3325
+ const text = i === cursor ? `${_B}${options[i]}${_R}` : `${_D}${options[i]}${_R}`;
3326
+ out.write(`${_cl()} ${arrow} ${box} ${text}
3327
+ `);
3328
+ }
3329
+ };
3330
+ out.write(_HIDE);
3331
+ render(false);
3332
+ await _readKeys((key) => {
3333
+ if (key === _UP_KEY && cursor > 0) {
3334
+ cursor--;
3335
+ render(true);
3336
+ } else if (key === _DOWN_KEY && cursor < options.length - 1) {
3337
+ cursor++;
3338
+ render(true);
3339
+ } else if (key === " ") {
3340
+ selected.has(cursor) ? selected.delete(cursor) : selected.add(cursor);
3341
+ render(true);
3342
+ } else if (key === "\r") {
3343
+ return true;
3344
+ }
3345
+ return false;
3346
+ });
3347
+ const picked = [...selected].sort((a, b) => a - b);
3348
+ const summary = picked.length > 0 ? picked.map((i) => options[i]).join(", ") : "none";
3349
+ out.write(_up(total));
3350
+ out.write(`${_cl()} ${question} ${_C}${_B}${summary}${_R}
3351
+ `);
3352
+ for (let i = 0;i < total - 1; i++)
3353
+ out.write(`${_cl()}
3354
+ `);
3355
+ out.write(_up(total - 1));
3356
+ out.write(_SHOW);
3357
+ return picked;
3358
+ }
3359
+ var PLUGINS = [
3360
+ { pkg: "@swc/plugin-emotion", version: "12.0.0", label: "Emotion (CSS-in-JS)" },
3361
+ { pkg: "@swc/plugin-styled-components", version: "10.0.0", label: "styled-components" },
3362
+ { pkg: "@swc/plugin-relay", version: "10.0.0", label: "Relay (GraphQL)" },
3363
+ { pkg: "@swc/plugin-styled-jsx", version: "11.0.0", label: "styled-jsx" },
3364
+ { pkg: "@swc/plugin-transform-imports", version: "10.0.0", label: "transform-imports" },
3365
+ { pkg: "@swc/plugin-loadable-components", version: "9.0.0", label: "Loadable Components" },
3366
+ { pkg: "@swc/plugin-formatjs", version: "7.0.0", label: "FormatJS (i18n)" }
3367
+ ];
3368
+ function renderSwcPluginsConfig(plugins) {
3369
+ if (plugins.length === 0)
3370
+ return "";
3371
+ const lines = plugins.map((p) => {
3372
+ if (p.pkg === "@swc/plugin-relay") {
3373
+ return ` ['${p.pkg}', { rootDir: process.cwd(), artifactDirectory: 'src/__generated__' }],`;
3374
+ }
3375
+ return ` ['${p.pkg}', {}],`;
3376
+ });
3377
+ return `
3378
+ swcPlugins: [
3379
+ ${lines.join(`
3380
+ `)}
3381
+ ],`;
3382
+ }
3383
+ function buildTemplates(name, opts) {
3384
+ const { useTypeScript, plugins } = opts;
3385
+ const appExt = useTypeScript ? "tsx" : "jsx";
3386
+ const cfgExt = useTypeScript ? "ts" : "js";
3387
+ const tsOrJs = useTypeScript ? "tsconfig.json" : "jsconfig.json";
3388
+ const pluginDeps = {};
3389
+ for (const p of plugins)
3390
+ pluginDeps[p.pkg] = p.version;
3391
+ const packageJson = JSON.stringify({
3189
3392
  name,
3190
3393
  version: "0.1.0",
3191
3394
  type: "module",
@@ -3199,19 +3402,28 @@ var TEMPLATES = {
3199
3402
  hadars: "latest",
3200
3403
  react: "^19.0.0",
3201
3404
  "react-dom": "^19.0.0"
3202
- }
3405
+ },
3406
+ ...Object.keys(pluginDeps).length > 0 ? { devDependencies: pluginDeps } : {}
3203
3407
  }, null, 2) + `
3204
- `,
3205
- "hadars.config.ts": () => `import type { HadarsOptions } from 'hadars';
3408
+ `;
3409
+ const swcSection = renderSwcPluginsConfig(plugins);
3410
+ const hadarsConfig = useTypeScript ? `import type { HadarsOptions } from 'hadars';
3206
3411
 
3207
3412
  const config: HadarsOptions = {
3208
3413
  entry: 'src/App.tsx',
3209
- port: 3000,
3414
+ port: 3000,${swcSection}
3210
3415
  };
3211
3416
 
3212
3417
  export default config;
3213
- `,
3214
- "tsconfig.json": () => JSON.stringify({
3418
+ ` : `/** @type {import('hadars').HadarsOptions} */
3419
+ const config = {
3420
+ entry: 'src/App.jsx',
3421
+ port: 3000,${swcSection}
3422
+ };
3423
+
3424
+ export default config;
3425
+ `;
3426
+ const tsConfigContent = useTypeScript ? JSON.stringify({
3215
3427
  compilerOptions: {
3216
3428
  lib: ["ESNext", "DOM"],
3217
3429
  target: "ESNext",
@@ -3226,215 +3438,184 @@ export default config;
3226
3438
  skipLibCheck: true
3227
3439
  }
3228
3440
  }, null, 2) + `
3229
- `,
3230
- ".gitignore": () => `node_modules/
3231
- .hadars/
3232
- dist/
3233
- `,
3234
- "src/App.tsx": () => `import React from 'react';
3235
- import { HadarsHead, type HadarsApp } from 'hadars';
3441
+ ` : JSON.stringify({
3442
+ compilerOptions: {
3443
+ lib: ["ESNext", "DOM"],
3444
+ target: "ESNext",
3445
+ module: "ESNext",
3446
+ moduleResolution: "bundler",
3447
+ jsx: "react-jsx",
3448
+ checkJs: true,
3449
+ noEmit: true,
3450
+ skipLibCheck: true
3451
+ }
3452
+ }, null, 2) + `
3453
+ `;
3454
+ const appImports = useTypeScript ? `import React from 'react';
3455
+ import { HadarsHead, type HadarsApp } from 'hadars';` : `import React from 'react';
3456
+ import { HadarsHead } from 'hadars';`;
3457
+ const appSignature = useTypeScript ? `const App: HadarsApp<{}> = () => {` : `const App = () => {`;
3458
+ const appContent = `${appImports}
3236
3459
 
3237
3460
  const css = \`
3238
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
3461
+ *, *::before, *::after { box-sizing: border-box; }
3239
3462
 
3240
- body {
3241
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
3242
- background: #0f0f13;
3243
- color: #e2e8f0;
3244
- min-height: 100vh;
3463
+ :root {
3464
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3465
+ line-height: 1.5;
3466
+ font-weight: 400;
3467
+ color-scheme: light dark;
3468
+ color: rgba(255, 255, 255, 0.87);
3469
+ background-color: #242424;
3470
+ font-synthesis: none;
3471
+ text-rendering: optimizeLegibility;
3472
+ -webkit-font-smoothing: antialiased;
3245
3473
  }
3246
3474
 
3247
- .nav {
3248
- display: flex;
3249
- align-items: center;
3250
- justify-content: space-between;
3251
- padding: 1rem 2rem;
3252
- border-bottom: 1px solid #1e1e2e;
3253
- }
3254
- .nav-brand { font-weight: 700; font-size: 1.1rem; color: #a78bfa; letter-spacing: -0.02em; }
3255
- .nav-links { display: flex; gap: 1.5rem; }
3256
- .nav-links a { color: #94a3b8; text-decoration: none; font-size: 0.9rem; }
3257
- .nav-links a:hover { color: #e2e8f0; }
3475
+ body { margin: 0; }
3258
3476
 
3259
- .hero {
3260
- text-align: center;
3261
- padding: 5rem 1rem 4rem;
3262
- max-width: 680px;
3477
+ #app {
3478
+ max-width: 1280px;
3263
3479
  margin: 0 auto;
3264
- }
3265
- .hero-badge {
3266
- display: inline-block;
3267
- background: #1e1a2e;
3268
- border: 1px solid #4c1d95;
3269
- color: #a78bfa;
3270
- font-size: 0.75rem;
3271
- font-weight: 600;
3272
- letter-spacing: 0.05em;
3273
- text-transform: uppercase;
3274
- padding: 0.3rem 0.8rem;
3275
- border-radius: 999px;
3276
- margin-bottom: 1.5rem;
3277
- }
3278
- .hero h1 {
3279
- font-size: clamp(2rem, 5vw, 3.25rem);
3280
- font-weight: 800;
3281
- letter-spacing: -0.03em;
3282
- line-height: 1.15;
3283
- margin-bottom: 1rem;
3284
- }
3285
- .hero h1 span { color: #a78bfa; }
3286
- .hero p {
3287
- font-size: 1.1rem;
3288
- color: #94a3b8;
3289
- line-height: 1.7;
3290
- margin-bottom: 2.5rem;
3291
- }
3292
- .hero-actions { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; }
3293
- .btn {
3294
- display: inline-flex;
3480
+ padding: 2rem;
3481
+ text-align: center;
3482
+ display: flex;
3483
+ flex-direction: column;
3295
3484
  align-items: center;
3296
- gap: 0.4rem;
3297
- padding: 0.65rem 1.4rem;
3485
+ justify-content: center;
3486
+ min-height: 100vh;
3487
+ }
3488
+
3489
+ .logos { display: flex; align-items: center; justify-content: center; gap: 1rem; margin-bottom: 1.5rem; }
3490
+
3491
+ .logo {
3492
+ height: 6em;
3493
+ padding: 1.5em;
3494
+ will-change: filter;
3495
+ transition: filter 300ms;
3496
+ }
3497
+ .logo-react { animation: spin 20s linear infinite; }
3498
+ .logo-react:hover { filter: drop-shadow(0 0 2em #61dafbaa); }
3499
+
3500
+ @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
3501
+ @media (prefers-reduced-motion: reduce) { .logo-react { animation: none; } }
3502
+
3503
+ h1 { font-size: 3.2em; line-height: 1.1; }
3504
+
3505
+ .card { padding: 2em; }
3506
+ .card p { color: #aaa; }
3507
+
3508
+ button {
3298
3509
  border-radius: 8px;
3299
- font-size: 0.9rem;
3300
- font-weight: 600;
3510
+ border: 1px solid transparent;
3511
+ padding: 0.6em 1.2em;
3512
+ font-size: 1em;
3513
+ font-weight: 500;
3514
+ font-family: inherit;
3515
+ background-color: #1a1a1a;
3301
3516
  cursor: pointer;
3302
- border: none;
3303
- transition: opacity 0.15s, transform 0.1s;
3304
- text-decoration: none;
3305
- }
3306
- .btn:hover { opacity: 0.85; transform: translateY(-1px); }
3307
- .btn:active { transform: translateY(0); }
3308
- .btn-primary { background: #7c3aed; color: #fff; }
3309
- .btn-ghost { background: #1e1e2e; color: #e2e8f0; border: 1px solid #2d2d3e; }
3517
+ transition: border-color 0.25s;
3518
+ }
3519
+ button:hover { border-color: #a78bfa; }
3520
+ button:focus-visible { outline: 4px auto -webkit-focus-ring-color; }
3310
3521
 
3311
- .features {
3312
- display: grid;
3313
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
3314
- gap: 1rem;
3315
- max-width: 900px;
3316
- margin: 0 auto 4rem;
3317
- padding: 0 1.5rem;
3318
- }
3319
- .card {
3320
- background: #16161f;
3321
- border: 1px solid #1e1e2e;
3322
- border-radius: 12px;
3323
- padding: 1.5rem;
3324
- }
3325
- .card-icon { font-size: 1.5rem; margin-bottom: 0.75rem; }
3326
- .card h3 { font-size: 0.95rem; font-weight: 700; margin-bottom: 0.4rem; }
3327
- .card p { font-size: 0.85rem; color: #64748b; line-height: 1.6; }
3522
+ .hint { color: #555; font-size: 0.9em; }
3523
+ .hint a { color: inherit; }
3524
+ .hint a:hover { color: #a78bfa; }
3328
3525
 
3329
- .demo {
3330
- max-width: 480px;
3331
- margin: 0 auto 4rem;
3332
- padding: 0 1.5rem;
3333
- text-align: center;
3526
+ @media (prefers-color-scheme: light) {
3527
+ :root { color: #213547; background-color: #ffffff; }
3528
+ button { background-color: #f9f9f9; }
3529
+ .card p { color: #666; }
3530
+ .hint { color: #999; }
3334
3531
  }
3335
- .demo-box {
3336
- background: #16161f;
3337
- border: 1px solid #1e1e2e;
3338
- border-radius: 12px;
3339
- padding: 2rem;
3340
- }
3341
- .demo-box h2 { font-size: 0.8rem; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 1.25rem; }
3342
- .counter { font-size: 3.5rem; font-weight: 800; color: #a78bfa; letter-spacing: -0.04em; margin-bottom: 1.25rem; }
3343
- .demo-actions { display: flex; gap: 0.75rem; justify-content: center; }
3344
-
3345
3532
  \`;
3346
3533
 
3347
- const App: HadarsApp<{}> = () => {
3534
+ ${appSignature}
3348
3535
  const [count, setCount] = React.useState(0);
3349
3536
 
3350
3537
  return (
3351
3538
  <>
3352
3539
  <HadarsHead status={200}>
3353
- <title>My App</title>
3540
+ <title>${name}</title>
3354
3541
  <meta name="viewport" content="width=device-width, initial-scale=1" />
3355
3542
  <style data-id="app-styles" dangerouslySetInnerHTML={{ __html: css }} />
3356
3543
  </HadarsHead>
3357
3544
 
3358
- <nav className="nav">
3359
- <span className="nav-brand">my-app</span>
3360
- <div className="nav-links">
3361
- <a href="https://github.com/dpostolachi/hadar" target="_blank" rel="noopener">github</a>
3545
+ <div id="app">
3546
+ <div className="logos">
3547
+ <a href="https://react.dev" target="_blank" rel="noopener noreferrer">
3548
+ <svg className="logo logo-react" xmlns="http://www.w3.org/2000/svg" viewBox="-11.5 -10.232 23 20.463" aria-label="React">
3549
+ <circle cx="0" cy="0" r="2.05" fill="#61dafb" />
3550
+ <g stroke="#61dafb" strokeWidth="1" fill="none">
3551
+ <ellipse rx="11" ry="4.2" />
3552
+ <ellipse rx="11" ry="4.2" transform="rotate(60)" />
3553
+ <ellipse rx="11" ry="4.2" transform="rotate(120)" />
3554
+ </g>
3555
+ </svg>
3556
+ </a>
3362
3557
  </div>
3363
- </nav>
3364
3558
 
3365
- <section className="hero">
3366
- <div className="hero-badge">built with hadars</div>
3367
- <h1>Ship <span>React apps</span><br />at full speed</h1>
3368
- <p>
3369
- SSR out of the box, zero config, instant hot-reload.
3370
- Edit <code>src/App.tsx</code> to get started.
3371
- </p>
3372
- <div className="hero-actions">
3373
- <button className="btn btn-primary" onClick={() => setCount(c => c + 1)}>
3374
- Try the counter ↓
3375
- </button>
3376
- </div>
3377
- </section>
3559
+ <h1>React + hadars</h1>
3378
3560
 
3379
- <div className="features">
3380
- <div className="card">
3381
- <div className="card-icon">⚡</div>
3382
- <h3>Server-side rendering</h3>
3383
- <p>Pages render on the server and hydrate on the client — great for SEO and first paint.</p>
3384
- </div>
3385
3561
  <div className="card">
3386
- <div className="card-icon">\uD83D\uDD25</div>
3387
- <h3>Hot module reload</h3>
3388
- <p>Changes in <code>src/App.tsx</code> reflect instantly in the browser during development.</p>
3389
- </div>
3390
- <div className="card">
3391
- <div className="card-icon">\uD83D\uDCE6</div>
3392
- <h3>Zero config</h3>
3393
- <p>One config file. Export a React component, run <code>hadars dev</code>, done.</p>
3394
- </div>
3395
- <div className="card">
3396
- <div className="card-icon">\uD83D\uDDC4️</div>
3397
- <h3>Server data hooks</h3>
3398
- <p>Use <code>useServerData</code> to fetch data on the server without extra round-trips.</p>
3562
+ <button onClick={() => setCount(c => c + 1)}>
3563
+ count is {count}
3564
+ </button>
3565
+ <p>
3566
+ Edit <code>src/App.${appExt}</code> and save to test HMR
3567
+ </p>
3399
3568
  </div>
3400
- </div>
3401
3569
 
3402
- <div className="demo">
3403
- <div className="demo-box">
3404
- <h2>Client interactivity works</h2>
3405
- <div className="counter">{count}</div>
3406
- <div className="demo-actions">
3407
- <button className="btn btn-ghost" onClick={() => setCount(c => c - 1)}>− dec</button>
3408
- <button className="btn btn-primary" onClick={() => setCount(c => c + 1)}>+ inc</button>
3409
- </div>
3410
- </div>
3570
+ <p className="hint">
3571
+ <a href="https://hadars.xyz" target="_blank" rel="noopener noreferrer">hadars docs</a>
3572
+ &nbsp;·&nbsp;
3573
+ <a href="https://react.dev" target="_blank" rel="noopener noreferrer">react docs</a>
3574
+ </p>
3411
3575
  </div>
3412
-
3413
3576
  </>
3414
3577
  );
3415
3578
  };
3416
3579
 
3417
3580
  export default App;
3418
- `
3419
- };
3581
+ `;
3582
+ return {
3583
+ "package.json": packageJson,
3584
+ [`hadars.config.${cfgExt}`]: hadarsConfig,
3585
+ [tsOrJs]: tsConfigContent,
3586
+ ".gitignore": `node_modules/
3587
+ .hadars/
3588
+ dist/
3589
+ `,
3590
+ [`src/App.${appExt}`]: appContent
3591
+ };
3592
+ }
3420
3593
  async function createProject(name, cwd) {
3421
3594
  const dir = resolve(cwd, name);
3422
3595
  if (existsSync3(dir)) {
3423
3596
  console.error(`Directory already exists: ${dir}`);
3424
3597
  process.exit(1);
3425
3598
  }
3426
- console.log(`Creating hadars project in ${dir}`);
3599
+ const useTypeScript = await promptRadio("Language?", ["TypeScript", "JavaScript"]) === 0;
3600
+ const selectedIndices = await promptMultiSelect("Select SWC plugins to enable (optional):", PLUGINS.map((p) => p.label));
3601
+ const plugins = selectedIndices.map((i) => PLUGINS[i]);
3602
+ console.log(`
3603
+ Creating hadars project in ${dir}`);
3427
3604
  await mkdir2(join2(dir, "src"), { recursive: true });
3428
- for (const [file, template] of Object.entries(TEMPLATES)) {
3429
- const content = template(name);
3605
+ const files = buildTemplates(name, { useTypeScript, plugins });
3606
+ for (const [file, content] of Object.entries(files)) {
3430
3607
  await writeFile2(join2(dir, file), content, "utf-8");
3431
3608
  console.log(` created ${file}`);
3432
3609
  }
3610
+ const installHint = plugins.length > 0 ? `
3611
+ # Also install selected SWC plugins:
3612
+ npm install -D ${plugins.map((p) => `${p.pkg}@${p.version}`).join(" ")}
3613
+ ` : "";
3433
3614
  console.log(`
3434
3615
  Done! Next steps:
3435
3616
 
3436
3617
  cd ${name}
3437
- npm install # or: bun install / pnpm install
3618
+ npm install # or: bun install / pnpm install${installHint}
3438
3619
  npm run dev # or: bun run dev
3439
3620
  `);
3440
3621
  }