webhanger 1.0.8 → 1.1.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 +599 -313
- package/bin/cli.js +304 -7
- package/core/devServer.js +337 -0
- package/helper/analyzer.js +32 -5
- package/helper/authConfig.js +68 -0
- package/helper/bundler.js +3 -2
- package/helper/dbHandler.js +11 -4
- package/helper/oauthHandler.js +149 -0
- package/helper/personalization.js +138 -0
- package/package.json +4 -2
package/bin/cli.js
CHANGED
|
@@ -396,11 +396,9 @@ switch (command) {
|
|
|
396
396
|
const manifestPath = pathMod.join(pathMod.dirname(atomFile), "wh-manifest.json");
|
|
397
397
|
await fsExtra.writeJson(manifestPath, manifest, { spaces: 2 });
|
|
398
398
|
|
|
399
|
-
// Step 4: Write globalJs to
|
|
399
|
+
// Step 4: Write globalJs to separate file
|
|
400
400
|
const globalJsFile = pathMod.join(pathMod.dirname(atomFile), "wh-global.js");
|
|
401
|
-
if (globalJs)
|
|
402
|
-
await fsExtra.writeFile(globalJsFile, globalJs, "utf-8");
|
|
403
|
-
}
|
|
401
|
+
if (globalJs) await fsExtra.writeFile(globalJsFile, globalJs, "utf-8");
|
|
404
402
|
|
|
405
403
|
// Step 5: Generate production-ready CDN-powered HTML
|
|
406
404
|
const mounts = components.map(c => ` <wh-component name="${c.name}"></wh-component>`).join("\n");
|
|
@@ -411,15 +409,24 @@ switch (command) {
|
|
|
411
409
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
412
410
|
<title>WebHanger</title>
|
|
413
411
|
${cdnAssets.filter(a => a.type === "style").map(a => `<link rel="stylesheet" href="${a.url}">`).join("\n ")}
|
|
412
|
+
<script src="https://unpkg.com/webhanger-front@latest/browser.min.js"></script>
|
|
414
413
|
</head>
|
|
415
414
|
<body>
|
|
416
415
|
${mounts}
|
|
417
416
|
${cdnAssets.filter(a => a.type === "script").map(a => `<script src="${a.url}"></script>`).join("\n ")}
|
|
418
|
-
<script src="https://unpkg.com/webhanger-front@latest/browser.min.js"></script>
|
|
419
417
|
<script>
|
|
418
|
+
var totalComponents = document.querySelectorAll("wh-component").length;
|
|
419
|
+
var mountedCount = 0;
|
|
420
|
+
WebHangerFront.on("afterMount", function() {
|
|
421
|
+
mountedCount++;
|
|
422
|
+
if (mountedCount >= totalComponents && !window.__whGlobalRan) {
|
|
423
|
+
window.__whGlobalRan = true;
|
|
424
|
+
${globalJs ? `var s = document.createElement("script"); s.src = "./wh-global.js"; document.body.appendChild(s);` : ""}
|
|
425
|
+
}
|
|
426
|
+
});
|
|
420
427
|
WebHangerFront.initialize("./wh-manifest.json");
|
|
421
428
|
</script>
|
|
422
|
-
${globalJs ?
|
|
429
|
+
${globalJs ? "" : ""}
|
|
423
430
|
</body>
|
|
424
431
|
</html>`;
|
|
425
432
|
|
|
@@ -453,6 +460,282 @@ ${mounts}
|
|
|
453
460
|
}
|
|
454
461
|
break;
|
|
455
462
|
}
|
|
463
|
+
case "dev": {
|
|
464
|
+
const devDir = args[1] || "./components";
|
|
465
|
+
const devPort = parseInt(args[2] || "4242");
|
|
466
|
+
const devManifest = args[3] || "./wh-manifest.json";
|
|
467
|
+
|
|
468
|
+
const loadConfigFn = (await import("../helper/loadConfig.js")).default;
|
|
469
|
+
const { startDevServer } = await import("../core/devServer.js");
|
|
470
|
+
|
|
471
|
+
const config = loadConfigFn();
|
|
472
|
+
|
|
473
|
+
console.log(chalk.cyan("\n⚡ wh dev — WebHanger Development Server\n"));
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
await startDevServer(config, devDir, {
|
|
477
|
+
port: devPort,
|
|
478
|
+
manifestOut: devManifest
|
|
479
|
+
});
|
|
480
|
+
} catch (err) {
|
|
481
|
+
console.log(chalk.red(`\n❌ Dev server failed: ${err.message}`));
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
case "personalize": {
|
|
487
|
+
const { default: fsExtra } = await import("fs-extra");
|
|
488
|
+
const { default: pathMod } = await import("path");
|
|
489
|
+
|
|
490
|
+
const subCmd = args[1];
|
|
491
|
+
|
|
492
|
+
if (subCmd === "init") {
|
|
493
|
+
// Scaffold a wh-personalization.json
|
|
494
|
+
const example = {
|
|
495
|
+
hero: {
|
|
496
|
+
rules: [
|
|
497
|
+
{ if: { country: "IN" }, component: "hero-india" },
|
|
498
|
+
{ if: { country: "US" }, component: "hero-us" },
|
|
499
|
+
{ if: { device: "mobile" }, component: "hero-mobile" },
|
|
500
|
+
{ if: { role: "premium" }, component: "hero-premium" },
|
|
501
|
+
{ if: { abTest: "variant-b" }, component: "hero-b" }
|
|
502
|
+
],
|
|
503
|
+
default: "hero"
|
|
504
|
+
},
|
|
505
|
+
navbar: {
|
|
506
|
+
rules: [
|
|
507
|
+
{ if: { role: "admin" }, component: "navbar-admin" },
|
|
508
|
+
{ if: { plan: "premium" }, component: "navbar-premium" }
|
|
509
|
+
],
|
|
510
|
+
default: "navbar"
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
const dest = pathMod.join(process.cwd(), "wh-personalization.json");
|
|
514
|
+
await fsExtra.writeJson(dest, example, { spaces: 2 });
|
|
515
|
+
console.log(chalk.green("\n✅ wh-personalization.json created"));
|
|
516
|
+
console.log(chalk.gray(" Edit the rules to match your use case."));
|
|
517
|
+
console.log(chalk.gray(" Supported conditions: country, device, role, abTest, lang, hour, plan\n"));
|
|
518
|
+
|
|
519
|
+
} else if (subCmd === "test") {
|
|
520
|
+
// Test rule resolution against current context
|
|
521
|
+
const { resolveAll } = await import("../helper/personalization.js");
|
|
522
|
+
const configPath = args[2] || "./wh-personalization.json";
|
|
523
|
+
if (!fsExtra.existsSync(configPath)) {
|
|
524
|
+
console.log(chalk.red(`${configPath} not found. Run: wh personalize init`));
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
const config = await fsExtra.readJson(configPath);
|
|
528
|
+
const country = args[3] || "US";
|
|
529
|
+
const device = args[4] || "desktop";
|
|
530
|
+
const role = args[5] || null;
|
|
531
|
+
|
|
532
|
+
const { resolved, ctx } = resolveAll(config, { country, device, role });
|
|
533
|
+
|
|
534
|
+
console.log(chalk.cyan("\n🎯 Personalization Resolution\n"));
|
|
535
|
+
console.log(chalk.white("Context:"));
|
|
536
|
+
console.log(chalk.gray(` country : ${ctx.country || "—"}`));
|
|
537
|
+
console.log(chalk.gray(` device : ${ctx.device}`));
|
|
538
|
+
console.log(chalk.gray(` role : ${ctx.role || "—"}`));
|
|
539
|
+
console.log(chalk.gray(` abTest : ${ctx.abTest}`));
|
|
540
|
+
console.log(chalk.gray(` lang : ${ctx.lang}`));
|
|
541
|
+
console.log(chalk.gray(` plan : ${ctx.plan}`));
|
|
542
|
+
console.log(chalk.white("\nResolved components:"));
|
|
543
|
+
for (const [slot, comp] of Object.entries(resolved)) {
|
|
544
|
+
console.log(chalk.gray(` ${slot.padEnd(15)} → ${comp}`));
|
|
545
|
+
}
|
|
546
|
+
console.log();
|
|
547
|
+
|
|
548
|
+
} else {
|
|
549
|
+
console.log(chalk.white("wh personalize commands:"));
|
|
550
|
+
console.log(chalk.gray(" wh personalize init — scaffold wh-personalization.json"));
|
|
551
|
+
console.log(chalk.gray(" wh personalize test [config] [country] [device] [role] — test rule resolution"));
|
|
552
|
+
}
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
case "auth": {
|
|
556
|
+
const authSubCmd = args[1];
|
|
557
|
+
|
|
558
|
+
if (authSubCmd === "init") {
|
|
559
|
+
// ── wh auth init ──────────────────────────────────────────────────
|
|
560
|
+
console.log(chalk.cyan("\n🔐 WebHanger Auth Setup\n"));
|
|
561
|
+
|
|
562
|
+
const { loadAuthConfig: _l, saveAuthConfig, generateSessionSecret, buildCallbackUrl, PROVIDERS } = await import("../helper/authConfig.js");
|
|
563
|
+
|
|
564
|
+
const providerChoices = [
|
|
565
|
+
{ name: "Google", value: "google", checked: true },
|
|
566
|
+
{ name: "GitHub", value: "github", checked: true },
|
|
567
|
+
{ name: "Facebook", value: "facebook", checked: false }
|
|
568
|
+
];
|
|
569
|
+
|
|
570
|
+
const answers = await inquirer.prompt([
|
|
571
|
+
{
|
|
572
|
+
type: "checkbox",
|
|
573
|
+
name: "providers",
|
|
574
|
+
message: "Select OAuth providers:",
|
|
575
|
+
choices: providerChoices,
|
|
576
|
+
validate: v => v.length > 0 || "Select at least one provider"
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
type: "input",
|
|
580
|
+
name: "baseUrl",
|
|
581
|
+
message: "Your app base URL (e.g. https://myapp.com):",
|
|
582
|
+
validate: v => v.startsWith("http") || "Must be a valid URL"
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
type: "input",
|
|
586
|
+
name: "callbackPath",
|
|
587
|
+
message: "OAuth callback path:",
|
|
588
|
+
default: "/auth/callback"
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
type: "input",
|
|
592
|
+
name: "port",
|
|
593
|
+
message: "Auth server port:",
|
|
594
|
+
default: "3001"
|
|
595
|
+
}
|
|
596
|
+
]);
|
|
597
|
+
|
|
598
|
+
const config = {
|
|
599
|
+
baseUrl: answers.baseUrl.replace(/\/$/, ""),
|
|
600
|
+
callbackPath: answers.callbackPath,
|
|
601
|
+
port: parseInt(answers.port),
|
|
602
|
+
providers: {},
|
|
603
|
+
session: {
|
|
604
|
+
secret: generateSessionSecret(),
|
|
605
|
+
expiresIn: "7d"
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// Collect credentials per provider
|
|
610
|
+
for (const provider of answers.providers) {
|
|
611
|
+
console.log(chalk.cyan(`\n⚙️ Configure ${provider.charAt(0).toUpperCase() + provider.slice(1)}`));
|
|
612
|
+
const cbUrl = buildCallbackUrl(answers.baseUrl, answers.callbackPath, provider, answers.port);
|
|
613
|
+
console.log(chalk.gray(` Callback URL: ${cbUrl}`));
|
|
614
|
+
|
|
615
|
+
const links = {
|
|
616
|
+
google: "https://console.cloud.google.com/apis/credentials",
|
|
617
|
+
github: "https://github.com/settings/developers",
|
|
618
|
+
facebook: "https://developers.facebook.com/apps"
|
|
619
|
+
};
|
|
620
|
+
console.log(chalk.gray(` Create app at: ${links[provider]}\n`));
|
|
621
|
+
|
|
622
|
+
const creds = await inquirer.prompt([
|
|
623
|
+
{ type: "input", name: "clientId", message: `${provider} Client ID:` },
|
|
624
|
+
{ type: "password", name: "clientSecret", message: `${provider} Client Secret:` }
|
|
625
|
+
]);
|
|
626
|
+
|
|
627
|
+
config.providers[provider] = {
|
|
628
|
+
clientId: creds.clientId,
|
|
629
|
+
clientSecret: creds.clientSecret,
|
|
630
|
+
scopes: PROVIDERS[provider].scopes,
|
|
631
|
+
callbackUrl: cbUrl
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const dest = await saveAuthConfig(config);
|
|
636
|
+
console.log(chalk.green(`\n✅ wh-auth.config.json created`));
|
|
637
|
+
console.log(chalk.white("\n📋 Add these callback URLs to your OAuth apps:"));
|
|
638
|
+
for (const [p, c] of Object.entries(config.providers)) {
|
|
639
|
+
console.log(chalk.gray(` ${p.padEnd(10)} → ${c.callbackUrl}`));
|
|
640
|
+
}
|
|
641
|
+
console.log(chalk.cyan(`\n▶ Start auth server: wh auth serve\n`));
|
|
642
|
+
|
|
643
|
+
} else if (authSubCmd === "serve") {
|
|
644
|
+
// ── wh auth serve ─────────────────────────────────────────────────
|
|
645
|
+
const { loadAuthConfig } = await import("../helper/authConfig.js");
|
|
646
|
+
const { buildAuthUrl, exchangeCode, fetchProfile, issueJWT, generateState } = await import("../helper/oauthHandler.js");
|
|
647
|
+
const http = await import("http");
|
|
648
|
+
|
|
649
|
+
const authConfig = loadAuthConfig();
|
|
650
|
+
const PORT = args[2] ? parseInt(args[2]) : authConfig.port || 3001;
|
|
651
|
+
const states = new Map(); // CSRF state store
|
|
652
|
+
|
|
653
|
+
const server = http.default.createServer(async (req, res) => {
|
|
654
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
655
|
+
const p = url.pathname;
|
|
656
|
+
|
|
657
|
+
// ── Initiate OAuth ────────────────────────────────────────────
|
|
658
|
+
const initMatch = p.match(/^\/auth\/([a-z]+)$/);
|
|
659
|
+
if (initMatch) {
|
|
660
|
+
const provider = initMatch[1];
|
|
661
|
+
if (!authConfig.providers[provider]) { res.writeHead(404); res.end("Provider not configured"); return; }
|
|
662
|
+
const state = generateState();
|
|
663
|
+
states.set(state, { provider, ts: Date.now() });
|
|
664
|
+
const authUrl = buildAuthUrl(provider, authConfig, state);
|
|
665
|
+
res.writeHead(302, { Location: authUrl });
|
|
666
|
+
res.end(); return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// ── OAuth callback ────────────────────────────────────────────
|
|
670
|
+
const cbMatch = p.match(/^\/auth\/callback\/([a-z]+)$/);
|
|
671
|
+
if (cbMatch) {
|
|
672
|
+
const provider = cbMatch[1];
|
|
673
|
+
const code = url.searchParams.get("code");
|
|
674
|
+
const state = url.searchParams.get("state");
|
|
675
|
+
|
|
676
|
+
if (!states.has(state)) { res.writeHead(400); res.end("Invalid state"); return; }
|
|
677
|
+
states.delete(state);
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
const accessToken = await exchangeCode(provider, code, authConfig);
|
|
681
|
+
const user = await fetchProfile(provider, accessToken);
|
|
682
|
+
const jwt = issueJWT(user, authConfig.session.secret, authConfig.session.expiresIn);
|
|
683
|
+
|
|
684
|
+
// Return HTML that posts token back to opener or redirects
|
|
685
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
686
|
+
res.end(`<!DOCTYPE html><html><body><script>
|
|
687
|
+
var user = ${JSON.stringify(user)};
|
|
688
|
+
var token = "${jwt}";
|
|
689
|
+
localStorage.setItem("wh_auth_token", token);
|
|
690
|
+
localStorage.setItem("wh_auth_user", JSON.stringify(user));
|
|
691
|
+
if (window.opener) {
|
|
692
|
+
window.opener.postMessage({ type: "WH_AUTH_SUCCESS", user, token }, "*");
|
|
693
|
+
window.close();
|
|
694
|
+
} else {
|
|
695
|
+
var redirect = sessionStorage.getItem("wh_auth_redirect") || "/";
|
|
696
|
+
window.location.href = redirect.replace("{{email}}", encodeURIComponent(user.email)).replace("{{name}}", encodeURIComponent(user.name));
|
|
697
|
+
}
|
|
698
|
+
</script></body></html>`);
|
|
699
|
+
} catch (err) {
|
|
700
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
701
|
+
res.end(`<!DOCTYPE html><html><body><script>
|
|
702
|
+
if (window.opener) {
|
|
703
|
+
window.opener.postMessage({ type: "WH_AUTH_ERROR", message: "${err.message}" }, "*");
|
|
704
|
+
window.close();
|
|
705
|
+
} else { document.body.innerText = "Auth error: ${err.message}"; }
|
|
706
|
+
</script></body></html>`);
|
|
707
|
+
}
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ── Health check ──────────────────────────────────────────────
|
|
712
|
+
if (p === "/health") {
|
|
713
|
+
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|
|
714
|
+
res.end(JSON.stringify({ ok: true, providers: Object.keys(authConfig.providers) }));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
res.writeHead(404); res.end("Not found");
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
server.listen(PORT, () => {
|
|
722
|
+
console.log(chalk.cyan(`\n🔐 WebHanger Auth Server\n`));
|
|
723
|
+
console.log(chalk.white(` Listening : http://localhost:${PORT}`));
|
|
724
|
+
console.log(chalk.gray(` Providers : ${Object.keys(authConfig.providers).join(", ")}`));
|
|
725
|
+
console.log(chalk.gray(`\n OAuth URLs:`));
|
|
726
|
+
for (const provider of Object.keys(authConfig.providers)) {
|
|
727
|
+
console.log(chalk.gray(` http://localhost:${PORT}/auth/${provider}`));
|
|
728
|
+
}
|
|
729
|
+
console.log();
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
} else {
|
|
733
|
+
console.log(chalk.white("wh auth commands:"));
|
|
734
|
+
console.log(chalk.gray(" wh auth init — setup OAuth providers"));
|
|
735
|
+
console.log(chalk.gray(" wh auth serve [port] — start OAuth callback server"));
|
|
736
|
+
}
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
456
739
|
case "access": {
|
|
457
740
|
const subCmd = args[1]; // grant | revoke | list
|
|
458
741
|
const loadConfigFn = (await import("../helper/loadConfig.js")).default;
|
|
@@ -842,7 +1125,7 @@ ${mounts}
|
|
|
842
1125
|
console.log(chalk.red("Usage: wh analyze <component-dir>"));
|
|
843
1126
|
process.exit(1);
|
|
844
1127
|
}
|
|
845
|
-
const { analyzeComponent } = await import("../helper/analyzer.js");
|
|
1128
|
+
const { analyzeComponent, autoGenerateComponentMeta } = await import("../helper/analyzer.js");
|
|
846
1129
|
const result = await analyzeComponent(dir);
|
|
847
1130
|
console.log(chalk.cyan("\n🔍 Component Analysis\n"));
|
|
848
1131
|
console.log(chalk.white(`Framework : ${result.framework}`));
|
|
@@ -854,12 +1137,26 @@ ${mounts}
|
|
|
854
1137
|
} else {
|
|
855
1138
|
console.log(chalk.gray(" none"));
|
|
856
1139
|
}
|
|
1140
|
+
|
|
1141
|
+
// Also run autoGenerate to show props
|
|
1142
|
+
const { meta } = await autoGenerateComponentMeta(dir);
|
|
1143
|
+
const propKeys = Object.keys(meta.props || {});
|
|
1144
|
+
if (propKeys.length) {
|
|
1145
|
+
console.log(chalk.white(`\nProps detected ({{wh.*}} placeholders):`));
|
|
1146
|
+
propKeys.forEach(k => {
|
|
1147
|
+
const p = meta.props[k];
|
|
1148
|
+
console.log(chalk.gray(` ${k.padEnd(20)} default: "${p.default || ""}"`));
|
|
1149
|
+
});
|
|
1150
|
+
console.log(chalk.green(`\n✅ webhanger.component.json updated with ${propKeys.length} props`));
|
|
1151
|
+
}
|
|
1152
|
+
console.log();
|
|
857
1153
|
break;
|
|
858
1154
|
}
|
|
859
1155
|
default:
|
|
860
1156
|
console.log(chalk.cyan(BANNER));
|
|
861
1157
|
console.log(chalk.white("Commands:"));
|
|
862
1158
|
console.log(chalk.gray(" wh init — setup your project"));
|
|
1159
|
+
console.log(chalk.gray(" wh dev [comp-dir] [port] [manifest-out] — dev server with hot reload + live preview"));
|
|
863
1160
|
console.log(chalk.gray(" wh ship <comp-dir> <site-dir> [version] [out-dir] — deploy + build + zip in one shot"));
|
|
864
1161
|
console.log(chalk.gray(" wh deploy <dir> <name> <version> — deploy a single component"));
|
|
865
1162
|
console.log(chalk.gray(" wh graph-deploy <comp-dir> [version] [out-dir] — deploy all + resolve dep graph"));
|