webhanger 1.0.0 → 1.0.4

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.
@@ -0,0 +1,190 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+
4
+ /**
5
+ * Reads html/css/js from a component folder.
6
+ */
7
+ async function readComponent(componentDir) {
8
+ const files = await fs.readdir(componentDir);
9
+ let html = "", css = "", js = "";
10
+
11
+ for (const file of files) {
12
+ const ext = path.extname(file).toLowerCase();
13
+ const content = await fs.readFile(path.join(componentDir, file), "utf-8");
14
+ if (ext === ".html") html = content.trim();
15
+ else if (ext === ".css") css = content.trim();
16
+ else if (ext === ".js") js = content.trim();
17
+ }
18
+
19
+ return { html, css, js };
20
+ }
21
+
22
+ // ─── Converters ───────────────────────────────────────────────────────────────
23
+
24
+ function toReact(name, html, css, js) {
25
+ const componentName = name.charAt(0).toUpperCase() + name.slice(1);
26
+ // Convert class= to className= for JSX
27
+ const jsx = html.replace(/\bclass=/g, "className=");
28
+
29
+ return `import React, { useEffect } from 'react';
30
+
31
+ const styles = \`
32
+ ${css}
33
+ \`;
34
+
35
+ export default function ${componentName}() {
36
+ useEffect(() => {
37
+ // Injected from original script.js
38
+ ${js || "// no script"}
39
+ }, []);
40
+
41
+ return (
42
+ <>
43
+ <style>{\`\${styles}\`}</style>
44
+ <div dangerouslySetInnerHTML={{ __html: \`${jsx.replace(/`/g, "\\`")}\` }} />
45
+ </>
46
+ );
47
+ }
48
+ `;
49
+ }
50
+
51
+ function toVue(name, html, css, js) {
52
+ return `<template>
53
+ <div v-html="markup" />
54
+ </template>
55
+
56
+ <script setup>
57
+ import { onMounted } from 'vue';
58
+
59
+ const markup = \`${html.replace(/`/g, "\\`")}\`;
60
+
61
+ onMounted(() => {
62
+ ${js || "// no script"}
63
+ });
64
+ </script>
65
+
66
+ <style scoped>
67
+ ${css}
68
+ </style>
69
+ `;
70
+ }
71
+
72
+ function toSvelte(name, html, css, js) {
73
+ return `<script>
74
+ import { onMount } from 'svelte';
75
+
76
+ onMount(() => {
77
+ ${js || "// no script"}
78
+ });
79
+ </script>
80
+
81
+ ${html}
82
+
83
+ <style>
84
+ ${css}
85
+ </style>
86
+ `;
87
+ }
88
+
89
+ function toNext(name, html, css, js) {
90
+ const componentName = name.charAt(0).toUpperCase() + name.slice(1);
91
+ const jsx = html.replace(/\bclass=/g, "className=");
92
+
93
+ return `'use client';
94
+ import { useEffect } from 'react';
95
+
96
+ const styles = \`
97
+ ${css}
98
+ \`;
99
+
100
+ export default function ${componentName}() {
101
+ useEffect(() => {
102
+ ${js || "// no script"}
103
+ }, []);
104
+
105
+ return (
106
+ <>
107
+ <style>{\`\${styles}\`}</style>
108
+ <div dangerouslySetInnerHTML={{ __html: \`${jsx.replace(/`/g, "\\`")}\` }} />
109
+ </>
110
+ );
111
+ }
112
+ `;
113
+ }
114
+
115
+ function toAngular(name, html, css, js) {
116
+ const componentName = name.charAt(0).toUpperCase() + name.slice(1);
117
+ const selector = `wh-${name.toLowerCase()}`;
118
+
119
+ return `import { Component, OnInit } from '@angular/core';
120
+
121
+ @Component({
122
+ selector: '${selector}',
123
+ template: \`${html.replace(/`/g, "\\`")}\`,
124
+ styles: [\`
125
+ ${css}
126
+ \`]
127
+ })
128
+ export class ${componentName}Component implements OnInit {
129
+ ngOnInit(): void {
130
+ ${js || "// no script"}
131
+ }
132
+ }
133
+ `;
134
+ }
135
+
136
+ function toAstro(name, html, css, js) {
137
+ return `---
138
+ // ${name} component — converted by WebHanger
139
+ ${js ? `
140
+ // Script logic moved to client-side
141
+ ` : ""}
142
+ ---
143
+
144
+ ${html}
145
+
146
+ <style>
147
+ ${css}
148
+ </style>
149
+
150
+ ${js ? `<script>
151
+ ${js}
152
+ </script>` : ""}
153
+ `;
154
+ }
155
+
156
+ // ─── Extension map ────────────────────────────────────────────────────────────
157
+
158
+ const CONVERTERS = {
159
+ react: { fn: toReact, ext: ".jsx" },
160
+ next: { fn: toNext, ext: ".jsx" },
161
+ vue: { fn: toVue, ext: ".vue" },
162
+ svelte: { fn: toSvelte, ext: ".svelte" },
163
+ angular: { fn: toAngular, ext: ".component.ts" },
164
+ astro: { fn: toAstro, ext: ".astro" },
165
+ };
166
+
167
+ /**
168
+ * Converts a vanilla html/css/js component to a target framework component.
169
+ *
170
+ * @param {string} componentDir - source folder with html/css/js
171
+ * @param {string} name - component name e.g. "navbar"
172
+ * @param {string} target - "react" | "vue" | "svelte" | "next" | "angular" | "astro"
173
+ * @param {string} outputDir - where to write the converted file
174
+ */
175
+ export async function convert(componentDir, name, target, outputDir = "./converted") {
176
+ const converter = CONVERTERS[target.toLowerCase()];
177
+ if (!converter) {
178
+ throw new Error(`Unknown target: "${target}". Supported: ${Object.keys(CONVERTERS).join(", ")}`);
179
+ }
180
+
181
+ const { html, css, js } = await readComponent(componentDir);
182
+ const code = converter.fn(name, html, css, js);
183
+ const fileName = `${name}${converter.ext}`;
184
+ const outPath = path.join(outputDir, fileName);
185
+
186
+ await fs.ensureDir(outputDir);
187
+ await fs.writeFile(outPath, code, "utf-8");
188
+
189
+ return { outPath, fileName, target, code };
190
+ }
@@ -0,0 +1,39 @@
1
+ import crypto from "crypto";
2
+
3
+ const ALGO = "aes-256-gcm";
4
+ const KEY_LEN = 32;
5
+ const IV_LEN = 12;
6
+
7
+ /**
8
+ * Derives a 256-bit AES key from SHA-256(projectId + salt)
9
+ * Simple, fast, consistent between Node and browser.
10
+ */
11
+ function deriveKey(projectId, salt) {
12
+ return crypto.createHash("sha256").update(projectId + salt).digest();
13
+ }
14
+
15
+ export function encrypt(plaintext, projectId, salt = "::wh") {
16
+ if (!plaintext) return "";
17
+ const key = deriveKey(projectId, salt);
18
+ const iv = crypto.randomBytes(IV_LEN);
19
+ const cipher = crypto.createCipheriv(ALGO, key, iv);
20
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
21
+ const tag = cipher.getAuthTag();
22
+ return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
23
+ }
24
+
25
+ export function decrypt(encoded, projectId, salt = "::wh") {
26
+ if (!encoded) return "";
27
+ const [ivB64, tagB64, dataB64] = encoded.split(":");
28
+ const key = deriveKey(projectId, salt);
29
+ const iv = Buffer.from(ivB64, "base64");
30
+ const tag = Buffer.from(tagB64, "base64");
31
+ const data = Buffer.from(dataB64, "base64");
32
+ const decipher = crypto.createDecipheriv(ALGO, key, iv);
33
+ decipher.setAuthTag(tag);
34
+ return Buffer.concat([decipher.update(data), decipher.final()]).toString("utf-8");
35
+ }
36
+
37
+ export function integrityHash(content) {
38
+ return crypto.createHash("sha256").update(content).digest("hex");
39
+ }
@@ -1,5 +1,6 @@
1
1
  import admin from "firebase-admin";
2
2
  import fs from "fs-extra";
3
+ import path from "path";
3
4
 
4
5
  // ─── Firebase ─────────────────────────────────────────────────────────────────
5
6
 
@@ -7,7 +8,20 @@ let firebaseDb = null;
7
8
 
8
9
  function getFirestore(serviceAccountPath) {
9
10
  if (firebaseDb) return firebaseDb;
10
- const serviceAccount = fs.readJsonSync(serviceAccountPath);
11
+ // Resolve path relative to cwd first, then walk up to find it
12
+ let resolvedPath = path.resolve(process.cwd(), serviceAccountPath);
13
+ if (!fs.existsSync(resolvedPath)) {
14
+ // Try resolving from parent directories
15
+ let dir = process.cwd();
16
+ for (let i = 0; i < 5; i++) {
17
+ const candidate = path.join(dir, path.basename(serviceAccountPath));
18
+ if (fs.existsSync(candidate)) { resolvedPath = candidate; break; }
19
+ const parent = path.dirname(dir);
20
+ if (parent === dir) break;
21
+ dir = parent;
22
+ }
23
+ }
24
+ const serviceAccount = fs.readJsonSync(resolvedPath);
11
25
  if (!admin.apps.length) {
12
26
  admin.initializeApp({ credential: admin.credential.cert(serviceAccount) });
13
27
  }
@@ -1,19 +1,34 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
 
4
- export default function loadConfig() {
5
- const configPath = path.join(process.cwd(), "webhanger.config.json");
4
+ export default function loadConfig(configPath = null) {
5
+ // 1. Explicit path passed in
6
+ // 2. WEBHANGER_CONFIG env var
7
+ // 3. Walk up from cwd until found
8
+ const candidates = [];
6
9
 
7
- if (!fs.existsSync(configPath)) {
8
- throw new Error("webhanger.config.json not found. Run `wh init` first.");
9
- }
10
+ if (configPath) candidates.push(path.resolve(configPath));
11
+ if (process.env.WEBHANGER_CONFIG) candidates.push(path.resolve(process.env.WEBHANGER_CONFIG));
10
12
 
11
- const raw = fs.readFileSync(configPath, "utf-8");
12
- const config = JSON.parse(raw);
13
+ // Walk up directory tree from cwd
14
+ let dir = process.cwd();
15
+ for (let i = 0; i < 5; i++) {
16
+ candidates.push(path.join(dir, "webhanger.config.json"));
17
+ const parent = path.dirname(dir);
18
+ if (parent === dir) break;
19
+ dir = parent;
20
+ }
13
21
 
14
- if (!config.projectId || !config.secretKey) {
15
- throw new Error("Invalid config: missing projectId or secretKey.");
22
+ for (const candidate of candidates) {
23
+ if (fs.existsSync(candidate)) {
24
+ const raw = fs.readFileSync(candidate, "utf-8");
25
+ const config = JSON.parse(raw);
26
+ if (!config.projectId || !config.secretKey) {
27
+ throw new Error(`Invalid config at ${candidate}: missing projectId or secretKey.`);
28
+ }
29
+ return config;
30
+ }
16
31
  }
17
32
 
18
- return config;
33
+ throw new Error("webhanger.config.json not found. Run `wh init` first.");
19
34
  }
package/index.js CHANGED
@@ -149,4 +149,9 @@ export { upload, remove } from "./helper/bucketHandler.js";
149
149
  export { registerComponent, getComponent } from "./helper/dbHandler.js";
150
150
  export { provisionBucket, provisionCloudFront } from "./helper/awsProvisioner.js";
151
151
  export { deploy } from "./core/registry.js";
152
+ export { resolve as resolveGraph } from "./core/resolver.js";
152
153
  export { default as loadConfig } from "./helper/loadConfig.js";
154
+ export { analyzeComponent, autoGenerateComponentMeta } from "./helper/analyzer.js";
155
+ export { convert } from "./helper/converter.js";
156
+ export { build } from "./core/builder.js";
157
+ export { grantAccess, revokeAccess, checkPermission, listAccess, generateApiKey } from "./helper/accessControl.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webhanger",
3
- "version": "1.0.0",
3
+ "version": "1.0.4",
4
4
  "description": "Component-as-a-Service platform — bundle, sign, and deliver UI components via edge CDN",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -18,16 +18,26 @@
18
18
  "scripts": {
19
19
  "test": "echo \"Error: no test specified\" && exit 1"
20
20
  },
21
- "keywords": ["cdn", "components", "caas", "edge", "ui"],
21
+ "keywords": [
22
+ "cdn",
23
+ "components",
24
+ "caas",
25
+ "edge",
26
+ "ui"
27
+ ],
22
28
  "author": "",
23
29
  "license": "ISC",
24
30
  "dependencies": {
25
- "@aws-sdk/client-s3": "^3.0.0",
26
31
  "@aws-sdk/client-cloudfront": "^3.0.0",
32
+ "@aws-sdk/client-s3": "^3.0.0",
27
33
  "@aws-sdk/s3-request-presigner": "^3.0.0",
34
+ "archiver": "^7.0.1",
28
35
  "chalk": "^5.6.2",
29
36
  "firebase-admin": "^12.0.0",
30
37
  "fs-extra": "^11.3.4",
31
38
  "inquirer": "^13.4.1"
39
+ },
40
+ "devDependencies": {
41
+ "terser": "^5.46.1"
32
42
  }
33
43
  }