@a13xu/lucid 1.9.5 → 1.11.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 +15 -1
- package/build/database.d.ts +32 -0
- package/build/database.js +38 -0
- package/build/index.js +282 -1
- package/build/instance.d.ts +9 -0
- package/build/instance.js +78 -0
- package/build/tools/e2e.d.ts +13 -0
- package/build/tools/e2e.js +109 -0
- package/build/tools/plan.d.ts +75 -0
- package/build/tools/plan.js +148 -0
- package/build/tools/webdev/accessibility-audit.d.ts +23 -0
- package/build/tools/webdev/accessibility-audit.js +214 -0
- package/build/tools/webdev/api-client.d.ts +24 -0
- package/build/tools/webdev/api-client.js +167 -0
- package/build/tools/webdev/design-tokens.d.ts +18 -0
- package/build/tools/webdev/design-tokens.js +375 -0
- package/build/tools/webdev/generate-component.d.ts +18 -0
- package/build/tools/webdev/generate-component.js +123 -0
- package/build/tools/webdev/index.d.ts +10 -0
- package/build/tools/webdev/index.js +10 -0
- package/build/tools/webdev/perf-hints.d.ts +24 -0
- package/build/tools/webdev/perf-hints.js +247 -0
- package/build/tools/webdev/responsive-layout.d.ts +18 -0
- package/build/tools/webdev/responsive-layout.js +229 -0
- package/build/tools/webdev/scaffold-page.d.ts +18 -0
- package/build/tools/webdev/scaffold-page.js +137 -0
- package/build/tools/webdev/security-scan.d.ts +23 -0
- package/build/tools/webdev/security-scan.js +247 -0
- package/build/tools/webdev/seo-meta.d.ts +24 -0
- package/build/tools/webdev/seo-meta.js +114 -0
- package/build/tools/webdev/test-generator.d.ts +18 -0
- package/build/tools/webdev/test-generator.js +269 -0
- package/package.json +1 -1
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Schema
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
export const SecurityScanSchema = z.object({
|
|
6
|
+
code: z.string().describe("Code snippet to scan for security vulnerabilities"),
|
|
7
|
+
language: z
|
|
8
|
+
.enum(["javascript", "typescript", "html", "vue"])
|
|
9
|
+
.describe("Code language"),
|
|
10
|
+
context: z
|
|
11
|
+
.enum(["frontend", "backend", "api"])
|
|
12
|
+
.describe("Execution context (affects which rules apply)"),
|
|
13
|
+
});
|
|
14
|
+
const RULES = [
|
|
15
|
+
// XSS
|
|
16
|
+
{
|
|
17
|
+
id: "xss-innerhtml",
|
|
18
|
+
category: "XSS",
|
|
19
|
+
severity: "critical",
|
|
20
|
+
contexts: ["frontend", "all"],
|
|
21
|
+
pattern: /\.innerHTML\s*=/g,
|
|
22
|
+
message: "Direct innerHTML assignment is vulnerable to XSS",
|
|
23
|
+
remediation: "Use textContent for text, or DOMPurify.sanitize() before setting innerHTML",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "xss-dangerouslysetinnerhtml",
|
|
27
|
+
category: "XSS",
|
|
28
|
+
severity: "critical",
|
|
29
|
+
contexts: ["frontend", "all"],
|
|
30
|
+
pattern: /dangerouslySetInnerHTML/g,
|
|
31
|
+
message: "dangerouslySetInnerHTML can enable XSS if content is not sanitized",
|
|
32
|
+
remediation: "Sanitize HTML with DOMPurify before using dangerouslySetInnerHTML={{ __html: sanitized }}",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "xss-v-html",
|
|
36
|
+
category: "XSS",
|
|
37
|
+
severity: "critical",
|
|
38
|
+
contexts: ["frontend", "all"],
|
|
39
|
+
pattern: /v-html\s*=/g,
|
|
40
|
+
message: "v-html renders raw HTML and is vulnerable to XSS",
|
|
41
|
+
remediation: "Sanitize with DOMPurify: v-html=\"sanitize(content)\" where sanitize = DOMPurify.sanitize",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "xss-document-write",
|
|
45
|
+
category: "XSS",
|
|
46
|
+
severity: "high",
|
|
47
|
+
contexts: ["frontend", "all"],
|
|
48
|
+
pattern: /document\.write\s*\(/g,
|
|
49
|
+
message: "document.write() can enable XSS and blocks page rendering",
|
|
50
|
+
remediation: "Use DOM manipulation APIs (createElement, appendChild) instead",
|
|
51
|
+
},
|
|
52
|
+
// Injection
|
|
53
|
+
{
|
|
54
|
+
id: "injection-eval",
|
|
55
|
+
category: "Code Injection",
|
|
56
|
+
severity: "critical",
|
|
57
|
+
contexts: ["frontend", "backend", "api", "all"],
|
|
58
|
+
pattern: /\beval\s*\(/g,
|
|
59
|
+
message: "eval() executes arbitrary code — critical injection vulnerability",
|
|
60
|
+
remediation: "Remove eval(). Use JSON.parse() for data, or a proper AST parser for code",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "injection-function-constructor",
|
|
64
|
+
category: "Code Injection",
|
|
65
|
+
severity: "critical",
|
|
66
|
+
contexts: ["frontend", "backend", "api", "all"],
|
|
67
|
+
pattern: /new\s+Function\s*\(/g,
|
|
68
|
+
message: "new Function() is equivalent to eval() — executes arbitrary code",
|
|
69
|
+
remediation: "Avoid dynamic code execution. Use configuration objects or strategy patterns",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "injection-settimeout-string",
|
|
73
|
+
category: "Code Injection",
|
|
74
|
+
severity: "high",
|
|
75
|
+
contexts: ["frontend", "backend", "api", "all"],
|
|
76
|
+
// setTimeout("code", ...) or setInterval("code", ...)
|
|
77
|
+
pattern: /(?:setTimeout|setInterval)\s*\(\s*["'`]/g,
|
|
78
|
+
message: "Passing a string to setTimeout/setInterval is equivalent to eval()",
|
|
79
|
+
remediation: "Use an arrow function: setTimeout(() => yourCode(), delay)",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "injection-sql-concat",
|
|
83
|
+
category: "SQL Injection",
|
|
84
|
+
severity: "critical",
|
|
85
|
+
contexts: ["backend", "api"],
|
|
86
|
+
// String concat into what looks like a SQL query
|
|
87
|
+
pattern: /(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)[^;]*\+\s*(?:req\.|request\.|params\.|query\.|body\.)/gi,
|
|
88
|
+
message: "String-concatenated SQL query is vulnerable to SQL injection",
|
|
89
|
+
remediation: "Use parameterized queries or a query builder (Knex, Prisma, TypeORM)",
|
|
90
|
+
},
|
|
91
|
+
// Exposed secrets
|
|
92
|
+
{
|
|
93
|
+
id: "secret-hardcoded-key",
|
|
94
|
+
category: "Exposed Secret",
|
|
95
|
+
severity: "critical",
|
|
96
|
+
contexts: ["frontend", "backend", "api", "all"],
|
|
97
|
+
// API key patterns: long hex/base64 strings assigned to a key variable
|
|
98
|
+
pattern: /(?:api[_-]?key|secret|password|token|private[_-]?key)\s*[:=]\s*["'][A-Za-z0-9+/=_\-]{16,}["']/gi,
|
|
99
|
+
message: "Hardcoded secret or API key detected in source code",
|
|
100
|
+
remediation: "Move secrets to environment variables. Use process.env.YOUR_SECRET and add to .gitignore/.env",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "secret-private-key-pem",
|
|
104
|
+
category: "Exposed Secret",
|
|
105
|
+
severity: "critical",
|
|
106
|
+
contexts: ["frontend", "backend", "api", "all"],
|
|
107
|
+
pattern: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g,
|
|
108
|
+
message: "Private key material embedded in source code",
|
|
109
|
+
remediation: "Store private keys in environment variables or a secrets manager (AWS Secrets Manager, Vault)",
|
|
110
|
+
},
|
|
111
|
+
// Open redirect
|
|
112
|
+
{
|
|
113
|
+
id: "open-redirect",
|
|
114
|
+
category: "Open Redirect",
|
|
115
|
+
severity: "high",
|
|
116
|
+
contexts: ["backend", "api", "frontend"],
|
|
117
|
+
// redirect() or res.redirect() with user-controlled param
|
|
118
|
+
pattern: /(?:res\.redirect|router\.push|window\.location)\s*\(\s*(?:req\.|request\.|params\.|query\.|body\.|\$route\.)/g,
|
|
119
|
+
message: "Potential open redirect: user-controlled URL used in redirect",
|
|
120
|
+
remediation: "Validate redirect URLs against an allowlist before redirecting",
|
|
121
|
+
},
|
|
122
|
+
// Prototype pollution
|
|
123
|
+
{
|
|
124
|
+
id: "prototype-pollution",
|
|
125
|
+
category: "Prototype Pollution",
|
|
126
|
+
severity: "high",
|
|
127
|
+
contexts: ["backend", "api", "all"],
|
|
128
|
+
pattern: /\[["']__proto__["']\]|\[["']constructor["']\]\s*\[["']prototype["']\]/g,
|
|
129
|
+
message: "Prototype pollution vector detected",
|
|
130
|
+
remediation: "Use Object.create(null) for untrusted data containers, or validate against Object.prototype keys",
|
|
131
|
+
},
|
|
132
|
+
// Unsafe deserialization
|
|
133
|
+
{
|
|
134
|
+
id: "unsafe-deserialize",
|
|
135
|
+
category: "Unsafe Deserialization",
|
|
136
|
+
severity: "high",
|
|
137
|
+
contexts: ["backend", "api"],
|
|
138
|
+
pattern: /require\s*\(\s*["']node-serialize["']\)|unserialize\s*\(/g,
|
|
139
|
+
message: "Unsafe deserialization can lead to remote code execution",
|
|
140
|
+
remediation: "Use JSON.parse() for data exchange. Avoid node-serialize with untrusted input",
|
|
141
|
+
},
|
|
142
|
+
// CSRF
|
|
143
|
+
{
|
|
144
|
+
id: "csrf-no-token",
|
|
145
|
+
category: "CSRF",
|
|
146
|
+
severity: "medium",
|
|
147
|
+
contexts: ["backend", "api"],
|
|
148
|
+
// POST/PUT/DELETE route handler without csrf token check
|
|
149
|
+
pattern: /router\s*\.\s*(?:post|put|patch|delete)\s*\(/g,
|
|
150
|
+
message: "Mutating HTTP route — verify CSRF protection is configured (csurf middleware or SameSite cookies)",
|
|
151
|
+
remediation: "Use csurf middleware or ensure SameSite=Strict/Lax cookie attribute is set for session cookies",
|
|
152
|
+
},
|
|
153
|
+
// Path traversal
|
|
154
|
+
{
|
|
155
|
+
id: "path-traversal",
|
|
156
|
+
category: "Path Traversal",
|
|
157
|
+
severity: "high",
|
|
158
|
+
contexts: ["backend", "api"],
|
|
159
|
+
pattern: /(?:readFile|createReadStream|readFileSync)\s*\([^)]*(?:req\.|request\.|params\.|query\.|body\.)/g,
|
|
160
|
+
message: "File path derived from user input — potential path traversal vulnerability",
|
|
161
|
+
remediation: "Validate and sanitize file paths: use path.resolve() + path.relative() to ensure path stays within allowed directory",
|
|
162
|
+
},
|
|
163
|
+
// Headers
|
|
164
|
+
{
|
|
165
|
+
id: "cors-wildcard",
|
|
166
|
+
category: "Insecure CORS",
|
|
167
|
+
severity: "medium",
|
|
168
|
+
contexts: ["backend", "api"],
|
|
169
|
+
pattern: /Access-Control-Allow-Origin['":\s]*[*]/g,
|
|
170
|
+
message: "Wildcard CORS origin allows any domain to make credentialed requests",
|
|
171
|
+
remediation: "Specify allowed origins explicitly: Access-Control-Allow-Origin: https://yourdomain.com",
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Analyzer
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
function scanCode(code, language, context) {
|
|
178
|
+
const lines = code.split("\n");
|
|
179
|
+
const issues = [];
|
|
180
|
+
for (const rule of RULES) {
|
|
181
|
+
const appliesToContext = rule.contexts.includes("all") || rule.contexts.includes(context);
|
|
182
|
+
if (!appliesToContext)
|
|
183
|
+
continue;
|
|
184
|
+
for (let i = 0; i < lines.length; i++) {
|
|
185
|
+
const line = lines[i];
|
|
186
|
+
// Skip comment lines
|
|
187
|
+
if (/^\s*\/\/|^\s*\*|^\s*#/.test(line))
|
|
188
|
+
continue;
|
|
189
|
+
const re = new RegExp(rule.pattern.source, rule.pattern.flags.replace("g", "") + "g");
|
|
190
|
+
let match;
|
|
191
|
+
while ((match = re.exec(line)) !== null) {
|
|
192
|
+
issues.push({
|
|
193
|
+
line: i + 1,
|
|
194
|
+
severity: rule.severity,
|
|
195
|
+
category: rule.category,
|
|
196
|
+
message: rule.message,
|
|
197
|
+
remediation: rule.remediation,
|
|
198
|
+
});
|
|
199
|
+
if (match.index === re.lastIndex)
|
|
200
|
+
re.lastIndex++;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
205
|
+
issues.sort((a, b) => order[a.severity] - order[b.severity] || a.line - b.line);
|
|
206
|
+
return issues;
|
|
207
|
+
}
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Formatter
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
const SEV_ICON = {
|
|
212
|
+
critical: "🔴",
|
|
213
|
+
high: "🟠",
|
|
214
|
+
medium: "🟡",
|
|
215
|
+
low: "🔵",
|
|
216
|
+
};
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Handler
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Example call:
|
|
221
|
+
// handleSecurityScan({ code: 'element.innerHTML = userInput;', language: "javascript", context: "frontend" })
|
|
222
|
+
export function handleSecurityScan(args) {
|
|
223
|
+
const { code, language, context } = args;
|
|
224
|
+
const issues = scanCode(code, language, context);
|
|
225
|
+
if (issues.length === 0) {
|
|
226
|
+
return (`✅ No security issues found (${language}, ${context} context).\n\n` +
|
|
227
|
+
`💡 Note: Automated scanning cannot replace a full security review. ` +
|
|
228
|
+
`This tool checks for common patterns (XSS, injection, exposed secrets, etc.). ` +
|
|
229
|
+
`Manual review and dynamic testing are still necessary.`);
|
|
230
|
+
}
|
|
231
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
232
|
+
for (const i of issues)
|
|
233
|
+
counts[i.severity]++;
|
|
234
|
+
const lines = [
|
|
235
|
+
`🔐 Security Scan — ${language} (${context} context)`,
|
|
236
|
+
`Found ${issues.length} issue(s): 🔴 ${counts.critical} critical 🟠 ${counts.high} high 🟡 ${counts.medium} medium 🔵 ${counts.low} low`,
|
|
237
|
+
``,
|
|
238
|
+
];
|
|
239
|
+
for (const issue of issues) {
|
|
240
|
+
lines.push(`${SEV_ICON[issue.severity]} Line ${issue.line} — ${issue.category}`, ` ${issue.message}`, ` Fix: ${issue.remediation}`, ``);
|
|
241
|
+
}
|
|
242
|
+
lines.push(`💡 Reasoning: Scanned for ${context} security patterns including XSS vectors, ` +
|
|
243
|
+
`injection points, hardcoded secrets, open redirects, and prototype pollution. ` +
|
|
244
|
+
`Address 🔴 critical issues immediately. ` +
|
|
245
|
+
`This static scan complements but does not replace DAST (dynamic testing) and manual code review.`);
|
|
246
|
+
return lines.join("\n");
|
|
247
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const SeoMetaSchema: z.ZodObject<{
|
|
3
|
+
title: z.ZodString;
|
|
4
|
+
description: z.ZodString;
|
|
5
|
+
keywords: z.ZodArray<z.ZodString, "many">;
|
|
6
|
+
page_type: z.ZodEnum<["article", "product", "landing", "home"]>;
|
|
7
|
+
url: z.ZodOptional<z.ZodString>;
|
|
8
|
+
image_url: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
description: string;
|
|
11
|
+
title: string;
|
|
12
|
+
keywords: string[];
|
|
13
|
+
page_type: "article" | "product" | "landing" | "home";
|
|
14
|
+
url?: string | undefined;
|
|
15
|
+
image_url?: string | undefined;
|
|
16
|
+
}, {
|
|
17
|
+
description: string;
|
|
18
|
+
title: string;
|
|
19
|
+
keywords: string[];
|
|
20
|
+
page_type: "article" | "product" | "landing" | "home";
|
|
21
|
+
url?: string | undefined;
|
|
22
|
+
image_url?: string | undefined;
|
|
23
|
+
}>;
|
|
24
|
+
export declare function handleSeoMeta(args: z.infer<typeof SeoMetaSchema>): string;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Schema
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
export const SeoMetaSchema = z.object({
|
|
6
|
+
title: z.string().describe("Page title"),
|
|
7
|
+
description: z.string().describe("Page meta description (≤160 chars recommended)"),
|
|
8
|
+
keywords: z.array(z.string()).describe("SEO keywords"),
|
|
9
|
+
page_type: z
|
|
10
|
+
.enum(["article", "product", "landing", "home"])
|
|
11
|
+
.describe("Page type for structured data"),
|
|
12
|
+
url: z.string().optional().describe("Canonical page URL"),
|
|
13
|
+
image_url: z.string().optional().describe("OG/Twitter image URL"),
|
|
14
|
+
});
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Helpers
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
function buildJsonLd(type, title, description, url, imageUrl) {
|
|
19
|
+
const base = {
|
|
20
|
+
"@context": "https://schema.org",
|
|
21
|
+
name: title,
|
|
22
|
+
description,
|
|
23
|
+
...(url ? { url } : {}),
|
|
24
|
+
...(imageUrl ? { image: imageUrl } : {}),
|
|
25
|
+
};
|
|
26
|
+
switch (type) {
|
|
27
|
+
case "article":
|
|
28
|
+
return {
|
|
29
|
+
...base,
|
|
30
|
+
"@type": "Article",
|
|
31
|
+
headline: title,
|
|
32
|
+
datePublished: new Date().toISOString().split("T")[0],
|
|
33
|
+
author: { "@type": "Person", name: "Author" },
|
|
34
|
+
};
|
|
35
|
+
case "product":
|
|
36
|
+
return {
|
|
37
|
+
...base,
|
|
38
|
+
"@type": "Product",
|
|
39
|
+
offers: {
|
|
40
|
+
"@type": "Offer",
|
|
41
|
+
priceCurrency: "USD",
|
|
42
|
+
price: "0.00",
|
|
43
|
+
availability: "https://schema.org/InStock",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
case "home":
|
|
47
|
+
return {
|
|
48
|
+
...base,
|
|
49
|
+
"@type": "WebSite",
|
|
50
|
+
potentialAction: {
|
|
51
|
+
"@type": "SearchAction",
|
|
52
|
+
target: `${url ?? ""}/search?q={search_term_string}`,
|
|
53
|
+
"query-input": "required name=search_term_string",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
default: // landing
|
|
57
|
+
return { ...base, "@type": "WebPage" };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Handler
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Example call:
|
|
64
|
+
// handleSeoMeta({ title: "Buy Widgets", description: "Best widgets online", keywords: ["widget", "shop"], page_type: "product", url: "https://example.com/widgets", image_url: "https://example.com/og.jpg" })
|
|
65
|
+
export function handleSeoMeta(args) {
|
|
66
|
+
const { title, description, keywords, page_type, url, image_url } = args;
|
|
67
|
+
const canonicalUrl = url ?? "https://example.com/YOUR_PAGE";
|
|
68
|
+
const ogImage = image_url ?? "https://example.com/og-image.jpg";
|
|
69
|
+
const keywordStr = keywords.join(", ");
|
|
70
|
+
const jsonLd = buildJsonLd(page_type, title, description, canonicalUrl, ogImage);
|
|
71
|
+
const jsonLdStr = JSON.stringify(jsonLd, null, 2);
|
|
72
|
+
const descWarning = description.length > 160
|
|
73
|
+
? `\n⚠️ Description is ${description.length} chars (recommended ≤160)`
|
|
74
|
+
: "";
|
|
75
|
+
const metaTags = `<!-- Primary Meta Tags -->
|
|
76
|
+
<title>${title}</title>
|
|
77
|
+
<meta name="title" content="${title}" />
|
|
78
|
+
<meta name="description" content="${description}" />
|
|
79
|
+
<meta name="keywords" content="${keywordStr}" />
|
|
80
|
+
<link rel="canonical" href="${canonicalUrl}" />
|
|
81
|
+
|
|
82
|
+
<!-- Open Graph / Facebook -->
|
|
83
|
+
<meta property="og:type" content="${page_type === "article" ? "article" : "website"}" />
|
|
84
|
+
<meta property="og:url" content="${canonicalUrl}" />
|
|
85
|
+
<meta property="og:title" content="${title}" />
|
|
86
|
+
<meta property="og:description" content="${description}" />
|
|
87
|
+
<meta property="og:image" content="${ogImage}" />
|
|
88
|
+
|
|
89
|
+
<!-- Twitter Card -->
|
|
90
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
91
|
+
<meta name="twitter:url" content="${canonicalUrl}" />
|
|
92
|
+
<meta name="twitter:title" content="${title}" />
|
|
93
|
+
<meta name="twitter:description" content="${description}" />
|
|
94
|
+
<meta name="twitter:image" content="${ogImage}" />
|
|
95
|
+
|
|
96
|
+
<!-- JSON-LD Structured Data -->
|
|
97
|
+
<script type="application/ld+json">
|
|
98
|
+
${jsonLdStr}
|
|
99
|
+
</script>`;
|
|
100
|
+
const lines = [
|
|
101
|
+
`✅ SEO meta for: ${title}${descWarning}`,
|
|
102
|
+
`📄 Page type: ${page_type} | Keywords: ${keywords.length}`,
|
|
103
|
+
``,
|
|
104
|
+
"```html",
|
|
105
|
+
metaTags,
|
|
106
|
+
"```",
|
|
107
|
+
``,
|
|
108
|
+
`💡 Reasoning: Generated complete SEO metadata including primary tags, Open Graph, ` +
|
|
109
|
+
`Twitter Card, and ${page_type} JSON-LD structured data. ` +
|
|
110
|
+
`Replace placeholder URLs with your actual canonical URL and OG image. ` +
|
|
111
|
+
`For Next.js, use the \`metadata\` export. For Nuxt, use \`useHead()\`.`,
|
|
112
|
+
];
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const TestGeneratorSchema: z.ZodObject<{
|
|
3
|
+
code: z.ZodString;
|
|
4
|
+
test_framework: z.ZodEnum<["vitest", "jest", "playwright"]>;
|
|
5
|
+
test_type: z.ZodEnum<["unit", "integration", "e2e"]>;
|
|
6
|
+
component_framework: z.ZodDefault<z.ZodOptional<z.ZodEnum<["vue", "react", "none"]>>>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
code: string;
|
|
9
|
+
test_framework: "vitest" | "jest" | "playwright";
|
|
10
|
+
test_type: "unit" | "integration" | "e2e";
|
|
11
|
+
component_framework: "vue" | "none" | "react";
|
|
12
|
+
}, {
|
|
13
|
+
code: string;
|
|
14
|
+
test_framework: "vitest" | "jest" | "playwright";
|
|
15
|
+
test_type: "unit" | "integration" | "e2e";
|
|
16
|
+
component_framework?: "vue" | "none" | "react" | undefined;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function handleTestGenerator(args: z.infer<typeof TestGeneratorSchema>): string;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Schema
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
export const TestGeneratorSchema = z.object({
|
|
6
|
+
code: z.string().describe("Source code of the function, component, or API handler to test"),
|
|
7
|
+
test_framework: z
|
|
8
|
+
.enum(["vitest", "jest", "playwright"])
|
|
9
|
+
.describe("Test framework to use"),
|
|
10
|
+
test_type: z
|
|
11
|
+
.enum(["unit", "integration", "e2e"])
|
|
12
|
+
.describe("Type of tests to generate"),
|
|
13
|
+
component_framework: z
|
|
14
|
+
.enum(["vue", "react", "none"])
|
|
15
|
+
.optional()
|
|
16
|
+
.default("none")
|
|
17
|
+
.describe("Frontend framework (for component tests)"),
|
|
18
|
+
});
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/** Extract a function/component name from source code for use in test labels */
|
|
23
|
+
function extractName(code) {
|
|
24
|
+
const patterns = [
|
|
25
|
+
/export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)/,
|
|
26
|
+
/export\s+const\s+(\w+)\s*=/,
|
|
27
|
+
/const\s+(\w+)\s*=.*=>/,
|
|
28
|
+
/class\s+(\w+)/,
|
|
29
|
+
/def\s+(\w+)\s*\(/,
|
|
30
|
+
];
|
|
31
|
+
for (const re of patterns) {
|
|
32
|
+
const m = code.match(re);
|
|
33
|
+
if (m?.[1])
|
|
34
|
+
return m[1];
|
|
35
|
+
}
|
|
36
|
+
return "subject";
|
|
37
|
+
}
|
|
38
|
+
function buildVitestUnit(name, code, component) {
|
|
39
|
+
if (component === "vue") {
|
|
40
|
+
return `import { describe, it, expect, vi } from "vitest";
|
|
41
|
+
import { mount } from "@vue/test-utils";
|
|
42
|
+
import ${name} from "./${name}.vue";
|
|
43
|
+
|
|
44
|
+
describe("${name}", () => {
|
|
45
|
+
it("renders without errors", () => {
|
|
46
|
+
const wrapper = mount(${name});
|
|
47
|
+
expect(wrapper.exists()).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("renders with default props", () => {
|
|
51
|
+
const wrapper = mount(${name}, {
|
|
52
|
+
props: {
|
|
53
|
+
// TODO: add required props
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("emits expected events", async () => {
|
|
60
|
+
const wrapper = mount(${name});
|
|
61
|
+
// TODO: trigger interaction
|
|
62
|
+
// await wrapper.find("button").trigger("click");
|
|
63
|
+
// expect(wrapper.emitted()).toHaveProperty("update");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("handles empty/null props gracefully", () => {
|
|
67
|
+
// TODO: mount with edge-case props
|
|
68
|
+
expect(() => mount(${name}, { props: {} })).not.toThrow();
|
|
69
|
+
});
|
|
70
|
+
});`;
|
|
71
|
+
}
|
|
72
|
+
if (component === "react") {
|
|
73
|
+
return `import { describe, it, expect, vi } from "vitest";
|
|
74
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
75
|
+
import { ${name} } from "./${name}";
|
|
76
|
+
|
|
77
|
+
describe("${name}", () => {
|
|
78
|
+
it("renders without crashing", () => {
|
|
79
|
+
const { container } = render(<${name} />);
|
|
80
|
+
expect(container).toBeTruthy();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("renders expected content", () => {
|
|
84
|
+
render(<${name} />);
|
|
85
|
+
// TODO: assert visible text/elements
|
|
86
|
+
// expect(screen.getByText("Expected Text")).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("handles user interaction", async () => {
|
|
90
|
+
const onAction = vi.fn();
|
|
91
|
+
render(<${name} onAction={onAction} />);
|
|
92
|
+
// TODO: trigger interaction
|
|
93
|
+
// fireEvent.click(screen.getByRole("button"));
|
|
94
|
+
// expect(onAction).toHaveBeenCalledOnce();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("handles edge cases (empty, null, undefined props)", () => {
|
|
98
|
+
// TODO: render with edge-case props
|
|
99
|
+
expect(() => render(<${name} />)).not.toThrow();
|
|
100
|
+
});
|
|
101
|
+
});`;
|
|
102
|
+
}
|
|
103
|
+
// Pure function
|
|
104
|
+
return `import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
105
|
+
import { ${name} } from "./${name}";
|
|
106
|
+
|
|
107
|
+
describe("${name}", () => {
|
|
108
|
+
// Happy path
|
|
109
|
+
it("returns correct result for typical input", () => {
|
|
110
|
+
// TODO: replace with real input/output
|
|
111
|
+
const result = ${name}(/* happy path args */);
|
|
112
|
+
expect(result).toBeDefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Edge cases
|
|
116
|
+
it("handles empty input", () => {
|
|
117
|
+
// TODO: empty/zero/null input
|
|
118
|
+
// expect(${name}("")).toEqual(/* expected */);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("handles boundary values", () => {
|
|
122
|
+
// TODO: boundary conditions
|
|
123
|
+
// expect(${name}(0)).toBe(/* expected */);
|
|
124
|
+
// expect(${name}(Number.MAX_SAFE_INTEGER)).toBe(/* expected */);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Error path
|
|
128
|
+
it("throws on invalid input", () => {
|
|
129
|
+
expect(() => ${name}(/* invalid args */)).toThrow();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Mocks
|
|
133
|
+
it("calls dependencies with correct arguments", () => {
|
|
134
|
+
const mockDep = vi.fn().mockReturnValue("mocked");
|
|
135
|
+
vi.mock("./${name}", () => ({ dependency: mockDep }));
|
|
136
|
+
${name}(/* args with mock */);
|
|
137
|
+
// expect(mockDep).toHaveBeenCalledWith(/* expected args */);
|
|
138
|
+
});
|
|
139
|
+
});`;
|
|
140
|
+
}
|
|
141
|
+
function buildJestUnit(name, code, component) {
|
|
142
|
+
const vitestVersion = buildVitestUnit(name, code, component);
|
|
143
|
+
return vitestVersion
|
|
144
|
+
.replace(/from "vitest"/g, 'from "@jest/globals"')
|
|
145
|
+
.replace(/import \{ describe, it, expect, vi, beforeEach \} from "@jest\/globals";/, `import { describe, it, expect, jest, beforeEach } from "@jest/globals";`)
|
|
146
|
+
.replace(/vi\.fn/g, "jest.fn")
|
|
147
|
+
.replace(/vi\.mock/g, "jest.mock")
|
|
148
|
+
.replace(/vi\.spyOn/g, "jest.spyOn");
|
|
149
|
+
}
|
|
150
|
+
function buildPlaywrightE2E(name) {
|
|
151
|
+
return `import { test, expect, type Page } from "@playwright/test";
|
|
152
|
+
|
|
153
|
+
// E2E tests for: ${name}
|
|
154
|
+
// These run in a real browser — start your dev server first.
|
|
155
|
+
|
|
156
|
+
test.describe("${name} — E2E", () => {
|
|
157
|
+
test.beforeEach(async ({ page }: { page: Page }) => {
|
|
158
|
+
// TODO: navigate to the page under test
|
|
159
|
+
await page.goto("http://localhost:3000/YOUR_ROUTE");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("page loads and shows expected content", async ({ page }: { page: Page }) => {
|
|
163
|
+
await expect(page).toHaveTitle(/TODO: expected title/);
|
|
164
|
+
// await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("primary user flow completes successfully", async ({ page }: { page: Page }) => {
|
|
168
|
+
// TODO: simulate the main user journey
|
|
169
|
+
// await page.getByLabel("Email").fill("user@example.com");
|
|
170
|
+
// await page.getByRole("button", { name: "Submit" }).click();
|
|
171
|
+
// await expect(page.getByText("Success")).toBeVisible();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("error state is shown for invalid input", async ({ page }: { page: Page }) => {
|
|
175
|
+
// TODO: trigger error state
|
|
176
|
+
// await page.getByRole("button", { name: "Submit" }).click();
|
|
177
|
+
// await expect(page.getByRole("alert")).toBeVisible();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("is accessible (no critical violations)", async ({ page }: { page: Page }) => {
|
|
181
|
+
// Requires @axe-core/playwright
|
|
182
|
+
// const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
|
|
183
|
+
// expect(accessibilityScanResults.violations).toEqual([]);
|
|
184
|
+
});
|
|
185
|
+
});`;
|
|
186
|
+
}
|
|
187
|
+
function buildIntegrationTest(name, framework) {
|
|
188
|
+
const importLine = framework === "vitest"
|
|
189
|
+
? 'import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";'
|
|
190
|
+
: 'import { describe, it, expect, jest, beforeAll, afterAll } from "@jest/globals";';
|
|
191
|
+
return `${importLine}
|
|
192
|
+
// Integration tests for: ${name}
|
|
193
|
+
// These tests use real implementations (DB, filesystem, network) or heavy mocks.
|
|
194
|
+
|
|
195
|
+
describe("${name} — Integration", () => {
|
|
196
|
+
beforeAll(async () => {
|
|
197
|
+
// TODO: set up test database / test server
|
|
198
|
+
// await setupTestDb();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
afterAll(async () => {
|
|
202
|
+
// TODO: tear down
|
|
203
|
+
// await cleanupTestDb();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("handles the full happy-path workflow", async () => {
|
|
207
|
+
// TODO: end-to-end through real layers
|
|
208
|
+
// const result = await ${name}(realDependency, realArgs);
|
|
209
|
+
// expect(result).toMatchObject({ status: "ok" });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("rolls back on error (atomicity)", async () => {
|
|
213
|
+
// TODO: inject failure, verify no side-effects persisted
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("respects authorization boundaries", async () => {
|
|
217
|
+
// TODO: call with unauthorized context, expect rejection
|
|
218
|
+
});
|
|
219
|
+
});`;
|
|
220
|
+
}
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Handler
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Example call:
|
|
225
|
+
// handleTestGenerator({ code: "export function add(a, b) { return a + b; }", test_framework: "vitest", test_type: "unit", component_framework: "none" })
|
|
226
|
+
export function handleTestGenerator(args) {
|
|
227
|
+
const { code, test_framework, test_type, component_framework } = args;
|
|
228
|
+
const name = extractName(code);
|
|
229
|
+
const cf = component_framework ?? "none";
|
|
230
|
+
let testCode;
|
|
231
|
+
let filename;
|
|
232
|
+
if (test_type === "e2e") {
|
|
233
|
+
testCode = buildPlaywrightE2E(name);
|
|
234
|
+
filename = `${name}.e2e.spec.ts`;
|
|
235
|
+
}
|
|
236
|
+
else if (test_type === "integration") {
|
|
237
|
+
testCode = buildIntegrationTest(name, test_framework);
|
|
238
|
+
filename = `${name}.integration.spec.ts`;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
// unit
|
|
242
|
+
if (test_framework === "jest") {
|
|
243
|
+
testCode = buildJestUnit(name, code, cf);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
testCode = buildVitestUnit(name, code, cf);
|
|
247
|
+
}
|
|
248
|
+
filename = `${name}.spec.ts`;
|
|
249
|
+
}
|
|
250
|
+
const lines = [
|
|
251
|
+
`✅ Tests generated for: ${name}`,
|
|
252
|
+
`📄 Filename: ${filename}`,
|
|
253
|
+
`🔧 Framework: ${test_framework} | Type: ${test_type} | Component: ${cf}`,
|
|
254
|
+
``,
|
|
255
|
+
"```typescript",
|
|
256
|
+
testCode,
|
|
257
|
+
"```",
|
|
258
|
+
``,
|
|
259
|
+
`💡 Reasoning: Generated ${test_type} test scaffold for \`${name}\` using ${test_framework}. ` +
|
|
260
|
+
`Covers: happy path, edge cases (empty/null/boundary), error path, and mock setup. ` +
|
|
261
|
+
`TODO comments mark where you must fill in concrete values. ` +
|
|
262
|
+
(test_type === "e2e"
|
|
263
|
+
? "Start your dev server before running playwright tests."
|
|
264
|
+
: test_type === "integration"
|
|
265
|
+
? "Set up a test database/environment in beforeAll."
|
|
266
|
+
: "Run with `npx vitest` or `npx jest` after filling in the TODOs."),
|
|
267
|
+
];
|
|
268
|
+
return lines.join("\n");
|
|
269
|
+
}
|